Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
fastlane/README.md
fastlane/report.xml
fastlane/test_output/*
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.xcscmblueprint
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
.DS_Store
*.dSYM
*.dSYM.zip
*.ipa
*/xcuserdata/*
Display.xcodeproj/*
View File
+23
View File
@@ -0,0 +1,23 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "Display",
module_name = "Display",
srcs = glob([
"Source/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/ObjCRuntimeUtils:ObjCRuntimeUtils",
"//submodules/UIKitRuntimeUtils:UIKitRuntimeUtils",
"//submodules/AppBundle:AppBundle",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/Markdown:Markdown",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,69 @@
import Foundation
import UIKit
import AsyncDisplayKit
class ASTransformLayer: CATransformLayer {
override var contents: Any? {
get {
return nil
} set(value) {
}
}
override var backgroundColor: CGColor? {
get {
return nil
} set(value) {
}
}
override func setNeedsLayout() {
}
override func layoutSublayers() {
}
}
class ASTransformView: UIView {
override class var layerClass: AnyClass {
return ASTransformLayer.self
}
}
open class ASTransformLayerNode: ASDisplayNode {
public override init() {
super.init()
self.setLayerBlock({
return ASTransformLayer()
})
}
}
open class ASTransformViewNode: ASDisplayNode {
public override init() {
super.init()
self.setViewBlock({
return ASTransformView()
})
}
}
open class ASTransformNode: ASDisplayNode {
public init(layerBacked: Bool = true) {
if layerBacked {
super.init()
self.setLayerBlock({
return ASTransformLayer()
})
} else {
super.init()
self.setViewBlock({
return ASTransformView()
})
}
}
}
@@ -0,0 +1,86 @@
import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
public func addAccessibilityChildren(of node: ASDisplayNode, container: Any, to list: inout [Any]) {
if node.isAccessibilityElement {
let element = UIAccessibilityElement(accessibilityContainer: container)
element.accessibilityFrame = UIAccessibility.convertToScreenCoordinates(node.bounds, in: node.view)
element.accessibilityLabel = node.accessibilityLabel
element.accessibilityValue = node.accessibilityValue
element.accessibilityTraits = node.accessibilityTraits
element.accessibilityHint = node.accessibilityHint
element.accessibilityIdentifier = node.accessibilityIdentifier
//node.accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates(node.bounds, node.view)
list.append(element)
} else if let accessibilityElements = node.accessibilityElements {
list.append(contentsOf: accessibilityElements)
}
}
public func smartInvertColorsEnabled() -> Bool {
if #available(iOSApplicationExtension 11.0, iOS 11.0, *), UIAccessibility.isInvertColorsEnabled {
return true
} else {
return false
}
}
public func isReduceMotionEnabled() -> Signal<Bool, NoError> {
return Signal { subscriber in
subscriber.putNext(UIAccessibility.isReduceMotionEnabled)
let observer = NotificationCenter.default.addObserver(forName: UIAccessibility.reduceMotionStatusDidChangeNotification, object: nil, queue: .main, using: { _ in
subscriber.putNext(UIAccessibility.isReduceMotionEnabled)
})
return ActionDisposable {
Queue.mainQueue().async {
NotificationCenter.default.removeObserver(observer)
}
}
} |> runOn(Queue.mainQueue())
}
public func isSpeakSelectionEnabled() -> Bool {
return UIAccessibility.isSpeakSelectionEnabled
}
public func isSpeakSelectionEnabledSignal() -> Signal<Bool, NoError> {
return Signal { subscriber in
subscriber.putNext(UIAccessibility.isSpeakSelectionEnabled)
let observer = NotificationCenter.default.addObserver(forName: UIAccessibility.speakSelectionStatusDidChangeNotification, object: nil, queue: .main, using: { _ in
subscriber.putNext(UIAccessibility.isSpeakSelectionEnabled)
})
return ActionDisposable {
Queue.mainQueue().async {
NotificationCenter.default.removeObserver(observer)
}
}
} |> runOn(Queue.mainQueue())
}
public func isBoldTextEnabled() -> Signal<Bool, NoError> {
return Signal { subscriber in
subscriber.putNext(UIAccessibility.isBoldTextEnabled)
let observer = NotificationCenter.default.addObserver(forName: UIAccessibility.boldTextStatusDidChangeNotification, object: nil, queue: .main, using: { _ in
subscriber.putNext(UIAccessibility.isBoldTextEnabled)
})
return ActionDisposable {
Queue.mainQueue().async {
NotificationCenter.default.removeObserver(observer)
}
}
}
|> runOn(Queue.mainQueue())
}
public func isReduceTransparencyEnabled() -> Bool {
UIAccessibility.isReduceTransparencyEnabled
}
@@ -0,0 +1,56 @@
import Foundation
import UIKit
import AsyncDisplayKit
public protocol AccessibilityFocusableNode {
func accessibilityElementDidBecomeFocused()
}
public final class AccessibilityAreaNode: ASDisplayNode {
public var activate: (() -> Bool)?
public var increment: (() -> Void)?
public var decrement: (() -> Void)?
public var focused: (() -> Void)?
override public init() {
super.init()
self.isAccessibilityElement = true
}
override public func accessibilityActivate() -> Bool {
return self.activate?() ?? false
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return nil
}
override public func accessibilityElementDidBecomeFocused() {
if let focused = self.focused {
focused()
} else {
var supernode = self.supernode
while true {
if let supernodeValue = supernode {
if let listItemNode = supernodeValue as? AccessibilityFocusableNode {
listItemNode.accessibilityElementDidBecomeFocused()
break
} else {
supernode = supernodeValue.supernode
}
} else {
break
}
}
}
}
override public func accessibilityIncrement() {
self.increment?()
}
override public func accessibilityDecrement() {
self.decrement?()
}
}
@@ -0,0 +1,185 @@
import Foundation
import UIKit
import AsyncDisplayKit
public enum ActionSheetButtonColor {
case accent
case destructive
case disabled
}
public enum ActionSheetButtonFont {
case `default`
case bold
}
public class ActionSheetButtonItem: ActionSheetItem {
public let title: String
public let color: ActionSheetButtonColor
public let font: ActionSheetButtonFont
public let enabled: Bool
public let action: () -> Void
public init(title: String, color: ActionSheetButtonColor = .accent, font: ActionSheetButtonFont = .default, enabled: Bool = true, action: @escaping () -> Void) {
self.title = title
self.color = color
self.font = font
self.enabled = enabled
self.action = action
}
public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
let node = ActionSheetButtonNode(theme: theme)
node.setItem(self)
return node
}
public func updateNode(_ node: ActionSheetItemNode) {
guard let node = node as? ActionSheetButtonNode else {
assertionFailure()
return
}
node.setItem(self)
node.requestLayoutUpdate()
}
}
public class ActionSheetButtonNode: ActionSheetItemNode {
private let theme: ActionSheetControllerTheme
private let defaultFont: UIFont
private let boldFont: UIFont
private var item: ActionSheetButtonItem?
private let button: HighlightTrackingButton
private let label: ImmediateTextNode
private let accessibilityArea: AccessibilityAreaNode
private var pointerInteraction: PointerInteraction?
override public init(theme: ActionSheetControllerTheme) {
self.theme = theme
self.defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0))
self.boldFont = Font.medium(floor(theme.baseFontSize * 20.0 / 17.0))
self.button = HighlightTrackingButton()
self.button.isAccessibilityElement = false
self.label = ImmediateTextNode()
self.label.isUserInteractionEnabled = false
self.label.maximumNumberOfLines = 1
self.label.displaysAsynchronously = false
self.label.truncationType = .end
self.accessibilityArea = AccessibilityAreaNode()
super.init(theme: theme)
self.view.addSubview(self.button)
self.label.isUserInteractionEnabled = false
self.addSubnode(self.label)
self.addSubnode(self.accessibilityArea)
self.button.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
strongSelf.setHighlighted(highlighted, animated: true)
}
}
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
self.accessibilityArea.activate = { [weak self] in
self?.buttonPressed()
return true
}
}
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
self.highlightedUpdated(highlighted)
if highlighted {
self.backgroundNode.backgroundColor = self.theme.itemHighlightedBackgroundColor
} else {
if animated {
UIView.animate(withDuration: 0.3, animations: {
self.backgroundNode.backgroundColor = self.theme.itemBackgroundColor
})
} else {
self.backgroundNode.backgroundColor = self.theme.itemBackgroundColor
}
}
}
override func performAction() {
self.buttonPressed()
}
public override func didLoad() {
super.didLoad()
self.pointerInteraction = PointerInteraction(node: self, style: .hover, willEnter: { [weak self] in
if let strongSelf = self {
strongSelf.setHighlighted(true, animated: false)
}
}, willExit: { [weak self] in
if let strongSelf = self {
strongSelf.setHighlighted(false, animated: false)
}
})
}
func setItem(_ item: ActionSheetButtonItem) {
self.item = item
let textColor: UIColor
let textFont: UIFont
switch item.color {
case .accent:
textColor = self.theme.standardActionTextColor
case .destructive:
textColor = self.theme.destructiveActionTextColor
case .disabled:
textColor = self.theme.disabledActionTextColor
}
switch item.font {
case .default:
textFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0))
case .bold:
textFont = Font.medium(floor(theme.baseFontSize * 20.0 / 17.0))
}
self.label.attributedText = NSAttributedString(string: item.title, font: textFont, textColor: textColor)
self.label.isAccessibilityElement = false
self.button.isEnabled = item.enabled
self.accessibilityArea.accessibilityLabel = item.title
var accessibilityTraits: UIAccessibilityTraits = [.button]
if !item.enabled {
accessibilityTraits.insert(.notEnabled)
}
self.accessibilityArea.accessibilityTraits = accessibilityTraits
}
public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let size = CGSize(width: constrainedSize.width, height: 57.0)
self.button.frame = CGRect(origin: CGPoint(), size: size)
let labelSize = self.label.updateLayout(CGSize(width: max(1.0, size.width - 10.0), height: size.height))
self.label.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - labelSize.width) / 2.0), y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize)
self.accessibilityArea.frame = CGRect(origin: CGPoint(), size: size)
self.updateInternalLayout(size, constrainedSize: constrainedSize)
return size
}
@objc func buttonPressed() {
if let item = self.item {
item.action()
}
}
}
@@ -0,0 +1,170 @@
import Foundation
import UIKit
import AsyncDisplayKit
public enum ActionSheetCheckboxStyle {
case `default`
case alignRight
}
public class ActionSheetCheckboxItem: ActionSheetItem {
public let title: String
public let label: String
public let value: Bool
public let style: ActionSheetCheckboxStyle
public let action: (Bool) -> Void
public init(title: String, label: String, value: Bool, style: ActionSheetCheckboxStyle = .default, action: @escaping (Bool) -> Void) {
self.title = title
self.label = label
self.value = value
self.style = style
self.action = action
}
public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
let node = ActionSheetCheckboxItemNode(theme: theme)
node.setItem(self)
return node
}
public func updateNode(_ node: ActionSheetItemNode) {
guard let node = node as? ActionSheetCheckboxItemNode else {
assertionFailure()
return
}
node.setItem(self)
node.requestLayoutUpdate()
}
}
public class ActionSheetCheckboxItemNode: ActionSheetItemNode {
private let defaultFont: UIFont
private let theme: ActionSheetControllerTheme
private var item: ActionSheetCheckboxItem?
private let button: HighlightTrackingButton
private let titleNode: ImmediateTextNode
private let labelNode: ImmediateTextNode
private let checkNode: ASImageNode
private let accessibilityArea: AccessibilityAreaNode
override public init(theme: ActionSheetControllerTheme) {
self.theme = theme
self.defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0))
self.button = HighlightTrackingButton()
self.button.isAccessibilityElement = false
self.titleNode = ImmediateTextNode()
self.titleNode.maximumNumberOfLines = 1
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = false
self.titleNode.isAccessibilityElement = false
self.labelNode = ImmediateTextNode()
self.labelNode.maximumNumberOfLines = 1
self.labelNode.isUserInteractionEnabled = false
self.labelNode.displaysAsynchronously = false
self.labelNode.isAccessibilityElement = false
self.checkNode = ASImageNode()
self.checkNode.isUserInteractionEnabled = false
self.checkNode.displaysAsynchronously = false
self.checkNode.image = generateImage(CGSize(width: 14.0, height: 12.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(theme.controlAccentColor.cgColor)
context.setLineWidth(2.0 - UIScreenPixel)
context.setLineCap(.round)
context.move(to: CGPoint(x: 13.0, y: 1.0))
context.addLine(to: CGPoint(x: 5.0, y: 11.0))
context.addLine(to: CGPoint(x: 1.0, y: 7.0))
context.strokePath()
})
self.checkNode.isAccessibilityElement = false
self.accessibilityArea = AccessibilityAreaNode()
super.init(theme: theme)
self.view.addSubview(self.button)
self.addSubnode(self.titleNode)
self.addSubnode(self.labelNode)
self.addSubnode(self.checkNode)
self.addSubnode(self.accessibilityArea)
self.button.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemHighlightedBackgroundColor
} else {
UIView.animate(withDuration: 0.3, animations: {
strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemBackgroundColor
})
}
}
}
self.accessibilityArea.activate = { [weak self] in
self?.buttonPressed()
return true
}
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
}
func setItem(_ item: ActionSheetCheckboxItem) {
self.item = item
let defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0))
self.titleNode.attributedText = NSAttributedString(string: item.title, font: defaultFont, textColor: self.theme.primaryTextColor)
self.labelNode.attributedText = NSAttributedString(string: item.label, font: defaultFont, textColor: self.theme.secondaryTextColor)
self.checkNode.isHidden = !item.value
self.accessibilityArea.accessibilityLabel = item.title
var accessibilityTraits: UIAccessibilityTraits = [.button]
if item.value {
accessibilityTraits.insert(.selected)
}
self.accessibilityArea.accessibilityTraits = accessibilityTraits
}
public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let size = CGSize(width: constrainedSize.width, height: 57.0)
self.button.frame = CGRect(origin: CGPoint(), size: size)
var titleOrigin: CGFloat = 50.0
var checkOrigin: CGFloat = 27.0
if let item = self.item, item.style == .alignRight {
titleOrigin = 24.0
checkOrigin = size.width - 22.0
}
let labelSize = self.labelNode.updateLayout(CGSize(width: size.width - 44.0 - 15.0 - 8.0, height: size.height))
let titleSize = self.titleNode.updateLayout(CGSize(width: size.width - 44.0 - labelSize.width - 15.0 - 8.0, height: size.height))
self.titleNode.frame = CGRect(origin: CGPoint(x: titleOrigin, y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize)
self.labelNode.frame = CGRect(origin: CGPoint(x: size.width - 15.0 - labelSize.width, y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize)
if let image = self.checkNode.image {
self.checkNode.frame = CGRect(origin: CGPoint(x: floor(checkOrigin - (image.size.width / 2.0)), y: floor((size.height - image.size.height) / 2.0)), size: image.size)
}
self.accessibilityArea.frame = CGRect(origin: CGPoint(), size: size)
self.updateInternalLayout(size, constrainedSize: constrainedSize)
return size
}
@objc func buttonPressed() {
if let item = self.item {
item.action(!item.value)
}
}
}
@@ -0,0 +1,137 @@
import Foundation
import UIKit
import AsyncDisplayKit
public protocol ActionSheetGroupOverlayNode: ASDisplayNode {
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition)
}
open class ActionSheetController: ViewController, PresentableController, StandalonePresentableController, KeyShortcutResponder {
private var actionSheetNode: ActionSheetControllerNode {
return self.displayNode as! ActionSheetControllerNode
}
public var theme: ActionSheetControllerTheme {
didSet {
if oldValue != self.theme {
self.actionSheetNode.theme = self.theme
}
}
}
private var groups: [ActionSheetItemGroup] = []
private var isDismissed: Bool = false
public var dismissed: ((Bool) -> Void)?
private var allowInputInset: Bool
public init(theme: ActionSheetControllerTheme, allowInputInset: Bool = false) {
self.theme = theme
self.allowInputInset = allowInputInset
super.init(navigationBarPresentationData: nil)
self.statusBar.statusBarStyle = .Ignore
self.blocksBackgroundWhenInOverlay = true
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func dismissAnimated() {
if !self.isDismissed {
self.isDismissed = true
self.actionSheetNode.animateOut(cancelled: false)
}
}
open override func loadDisplayNode() {
self.displayNode = ActionSheetControllerNode(theme: self.theme, allowInputInset: self.allowInputInset)
self.displayNodeDidLoad()
self.actionSheetNode.dismiss = { [weak self] cancelled in
self?.dismissed?(cancelled)
self?.presentingViewController?.dismiss(animated: false)
}
self.actionSheetNode.setGroups(self.groups)
}
override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.actionSheetNode.containerLayoutUpdated(layout, transition: transition)
}
open override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.viewDidAppear(completion: {})
}
public func viewDidAppear(completion: @escaping () -> Void) {
self.actionSheetNode.animateIn(completion: completion)
}
public func setItemGroups(_ groups: [ActionSheetItemGroup]) {
self.groups = groups
if self.isViewLoaded {
self.actionSheetNode.setGroups(groups)
}
}
public func updateItem(groupIndex: Int, itemIndex: Int, _ f: (ActionSheetItem) -> ActionSheetItem) {
if self.isViewLoaded {
self.actionSheetNode.updateItem(groupIndex: groupIndex, itemIndex: itemIndex, f)
}
}
public func setItemGroupOverlayNode(groupIndex: Int, node: ActionSheetGroupOverlayNode) {
if self.isViewLoaded {
self.actionSheetNode.setItemGroupOverlayNode(groupIndex: groupIndex, node: node)
}
}
public var keyShortcuts: [KeyShortcut] {
return [
KeyShortcut(
input: UIKeyCommand.inputEscape,
modifiers: [],
action: { [weak self] in
self?.dismissAnimated()
}
),
KeyShortcut(
input: "W",
modifiers: [.command],
action: { [weak self] in
self?.dismissAnimated()
}
),
KeyShortcut(
input: "\r",
modifiers: [],
action: { [weak self] in
self?.actionSheetNode.performHighlightedAction()
}
),
KeyShortcut(
input: UIKeyCommand.inputUpArrow,
modifiers: [],
action: { [weak self] in
self?.actionSheetNode.decreaseHighlightedIndex()
}
),
KeyShortcut(
input: UIKeyCommand.inputDownArrow,
modifiers: [],
action: { [weak self] in
self?.actionSheetNode.increaseHighlightedIndex()
}
)
]
}
}
@@ -0,0 +1,241 @@
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
private let containerInsets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
final class ActionSheetControllerNode: ASDisplayNode, ASScrollViewDelegate {
var theme: ActionSheetControllerTheme {
didSet {
self.itemGroupsContainerNode.theme = self.theme
self.updateTheme()
}
}
private var allowInputInset: Bool
private let dismissTapView: UIView
private let leftDimView: UIView
private let rightDimView: UIView
private let topDimView: UIView
private let bottomDimView: UIView
private let itemGroupsContainerNode: ActionSheetItemGroupsContainerNode
private let scrollNode: ASScrollNode
private let scrollView: UIScrollView
var dismiss: (Bool) -> Void = { _ in }
private var validLayout: ContainerViewLayout?
init(theme: ActionSheetControllerTheme, allowInputInset: Bool) {
self.theme = theme
self.allowInputInset = allowInputInset
self.scrollNode = ASScrollNode()
self.scrollNode.canCancelAllTouchesInViews = true
self.scrollView = self.scrollNode.view
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollView.contentInsetAdjustmentBehavior = .never
}
self.scrollView.alwaysBounceVertical = true
self.scrollView.delaysContentTouches = false
self.scrollView.canCancelContentTouches = true
self.dismissTapView = UIView()
self.leftDimView = UIView()
self.leftDimView.isUserInteractionEnabled = false
self.rightDimView = UIView()
self.rightDimView.isUserInteractionEnabled = false
self.topDimView = UIView()
self.topDimView.isUserInteractionEnabled = false
self.bottomDimView = UIView()
self.bottomDimView.isUserInteractionEnabled = false
self.itemGroupsContainerNode = ActionSheetItemGroupsContainerNode(theme: self.theme)
self.itemGroupsContainerNode.isUserInteractionEnabled = false
super.init()
self.scrollView.delegate = self.wrappedScrollViewDelegate
self.addSubnode(self.scrollNode)
self.scrollView.addSubview(self.dismissTapView)
self.scrollView.addSubview(self.leftDimView)
self.scrollView.addSubview(self.rightDimView)
self.scrollView.addSubview(self.topDimView)
self.scrollView.addSubview(self.bottomDimView)
self.dismissTapView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimNodeTap(_:))))
self.scrollNode.addSubnode(self.itemGroupsContainerNode)
self.updateTheme()
self.itemGroupsContainerNode.requestLayout = { [weak self] in
if let strongSelf = self, let layout = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, transition: .animated(duration: 0.2, curve: .easeInOut))
}
}
}
func performHighlightedAction() {
self.itemGroupsContainerNode.performHighlightedAction()
}
func decreaseHighlightedIndex() {
self.itemGroupsContainerNode.decreaseHighlightedIndex()
}
func increaseHighlightedIndex() {
self.itemGroupsContainerNode.increaseHighlightedIndex()
}
func updateTheme() {
self.leftDimView.backgroundColor = self.theme.dimColor
self.rightDimView.backgroundColor = self.theme.dimColor
self.topDimView.backgroundColor = self.theme.dimColor
self.bottomDimView.backgroundColor = self.theme.dimColor
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
var insets = layout.insets(options: [.statusBar])
let containerWidth = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left)
insets.left = floor((layout.size.width - containerWidth) / 2.0)
insets.right = insets.left
if !insets.bottom.isZero {
insets.bottom -= 12.0
}
if self.allowInputInset, let inputInset = layout.inputHeight, inputInset > 0.0 {
insets.bottom = inputInset
}
self.validLayout = layout
self.scrollView.frame = CGRect(origin: CGPoint(), size: layout.size)
self.dismissTapView.frame = CGRect(origin: CGPoint(), size: layout.size)
let itemGroupsContainerSize = self.itemGroupsContainerNode.updateLayout(constrainedSize: CGSize(width: layout.size.width - containerInsets.left - containerInsets.right - insets.left - insets.right, height: layout.size.height - containerInsets.top - containerInsets.bottom - insets.top - insets.bottom), transition: transition)
if self.allowInputInset, let inputHeight = layout.inputHeight, inputHeight > 0.0, self.itemGroupsContainerNode.groupNodes.count > 1, let lastGroupHeight = self.itemGroupsContainerNode.groupNodes.last?.frame.height {
insets.bottom -= lastGroupHeight + containerInsets.bottom
}
var transition = transition
if !self.allowInputInset {
transition = .immediate
}
transition.updateFrame(node: self.itemGroupsContainerNode, frame: CGRect(origin: CGPoint(x: insets.left + containerInsets.left, y: layout.size.height - insets.bottom - containerInsets.bottom - itemGroupsContainerSize.height), size: itemGroupsContainerSize))
self.updateScrollDimViews(size: layout.size, insets: insets, transition: transition)
}
func animateIn(completion: @escaping () -> Void) {
let tempDimView = UIView()
tempDimView.backgroundColor = self.theme.dimColor
tempDimView.frame = self.bounds.offsetBy(dx: 0.0, dy: -self.bounds.size.height)
self.view.addSubview(tempDimView)
for node in [tempDimView, self.topDimView, self.leftDimView, self.rightDimView, self.bottomDimView] {
node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
self.itemGroupsContainerNode.animateDimViewsAlpha(from: 0.0, to: 1.0, duration: 0.4)
self.layer.animateBounds(from: self.bounds.offsetBy(dx: 0.0, dy: -self.bounds.size.height), to: self.bounds, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak tempDimView] _ in
tempDimView?.removeFromSuperview()
completion()
})
Queue.mainQueue().after(0.3, {
self.itemGroupsContainerNode.isUserInteractionEnabled = true
})
}
func animateOut(cancelled: Bool) {
let tempDimView = UIView()
tempDimView.backgroundColor = self.theme.dimColor
tempDimView.frame = self.bounds.offsetBy(dx: 0.0, dy: -self.bounds.size.height)
self.view.addSubview(tempDimView)
for node in [tempDimView, self.topDimView, self.leftDimView, self.rightDimView, self.bottomDimView] {
node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
}
self.itemGroupsContainerNode.animateDimViewsAlpha(from: 1.0, to: 0.0, duration: 0.3)
self.layer.animateBounds(from: self.bounds, to: self.bounds.offsetBy(dx: 0.0, dy: -self.bounds.size.height), duration: 0.35, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self, weak tempDimView] _ in
tempDimView?.removeFromSuperview()
self?.dismiss(cancelled)
})
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
return result
}
@objc func dimNodeTap(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state, self.itemGroupsContainerNode.isUserInteractionEnabled {
self.view.window?.endEditing(true)
self.animateOut(cancelled: true)
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if let layout = self.validLayout {
var insets = layout.insets(options: [.statusBar])
let containerWidth = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left)
insets.left = floor((layout.size.width - containerWidth) / 2.0)
insets.right = insets.left
self.updateScrollDimViews(size: layout.size, insets: insets, transition: .immediate)
}
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
let contentOffset = self.scrollView.contentOffset
let additionalTopHeight = max(0.0, -contentOffset.y)
if additionalTopHeight >= 30.0 {
self.animateOut(cancelled: true)
}
}
func updateScrollDimViews(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) {
let additionalTopHeight = max(0.0, -self.scrollView.contentOffset.y)
let additionalBottomHeight = -min(0.0, -self.scrollView.contentOffset.y)
transition.updateFrame(view: self.topDimView, frame: CGRect(x: containerInsets.left + insets.left, y: -additionalTopHeight, width: size.width - containerInsets.left - containerInsets.right - insets.left - insets.right, height: max(0.0, self.itemGroupsContainerNode.frame.minY + additionalTopHeight)))
transition.updateFrame(view: self.bottomDimView, frame: CGRect(x: containerInsets.left + insets.left, y: self.itemGroupsContainerNode.frame.maxY, width: size.width - containerInsets.left - containerInsets.right - insets.left - insets.right, height: max(0.0, size.height - self.itemGroupsContainerNode.frame.maxY + additionalBottomHeight)))
transition.updateFrame(view: self.leftDimView, frame: CGRect(x: 0.0, y: -additionalTopHeight, width: containerInsets.left + insets.left, height: size.height + additionalTopHeight + additionalBottomHeight))
transition.updateFrame(view: self.rightDimView, frame: CGRect(x: size.width - containerInsets.right - insets.right, y: -additionalTopHeight, width: containerInsets.right + insets.right, height: size.height + additionalTopHeight + additionalBottomHeight))
}
func setGroups(_ groups: [ActionSheetItemGroup]) {
self.itemGroupsContainerNode.setGroups(groups)
}
func updateItem(groupIndex: Int, itemIndex: Int, _ f: (ActionSheetItem) -> ActionSheetItem) {
self.itemGroupsContainerNode.updateItem(groupIndex: groupIndex, itemIndex: itemIndex, f)
}
func setItemGroupOverlayNode(groupIndex: Int, node: ActionSheetGroupOverlayNode) {
self.itemGroupsContainerNode.setItemGroupOverlayNode(groupIndex: groupIndex, node: node)
}
}
@@ -0,0 +1,6 @@
import Foundation
public protocol ActionSheetItem {
func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode
func updateNode(_ node: ActionSheetItemNode) -> Void
}
@@ -0,0 +1,11 @@
import UIKit
public final class ActionSheetItemGroup {
let items: [ActionSheetItem]
let leadingVisibleNodeCount: CGFloat?
public init(items: [ActionSheetItem], leadingVisibleNodeCount: CGFloat? = nil) {
self.items = items
self.leadingVisibleNodeCount = leadingVisibleNodeCount
}
}
@@ -0,0 +1,236 @@
import UIKit
import AsyncDisplayKit
final class ActionSheetItemGroupNode: ASDisplayNode, ASScrollViewDelegate {
private let theme: ActionSheetControllerTheme
private let centerDimView: UIImageView
private let topDimView: UIView
private let bottomDimView: UIView
let trailingDimView: UIView
private let clippingNode: ASDisplayNode
private let backgroundEffectView: UIVisualEffectView
private let scrollNode: ASScrollNode
var itemNodes: [ActionSheetItemNode] = []
private var leadingVisibleNodeCount: CGFloat = 100.0
private var validLayout: CGSize?
private var overlayNode: ActionSheetGroupOverlayNode?
init(theme: ActionSheetControllerTheme) {
self.theme = theme
self.centerDimView = UIImageView()
self.centerDimView.image = generateStretchableFilledCircleImage(radius: 16.0, color: nil, backgroundColor: self.theme.dimColor)
self.topDimView = UIView()
self.topDimView.backgroundColor = self.theme.dimColor
self.topDimView.isUserInteractionEnabled = false
self.bottomDimView = UIView()
self.bottomDimView.backgroundColor = self.theme.dimColor
self.bottomDimView.isUserInteractionEnabled = false
self.trailingDimView = UIView()
self.trailingDimView.backgroundColor = self.theme.dimColor
self.clippingNode = ASDisplayNode()
self.clippingNode.clipsToBounds = true
self.clippingNode.cornerRadius = 16.0
self.backgroundEffectView = UIVisualEffectView(effect: UIBlurEffect(style: self.theme.backgroundType == .light ? .light : .dark))
self.scrollNode = ASScrollNode()
self.scrollNode.canCancelAllTouchesInViews = true
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
}
self.scrollNode.view.delaysContentTouches = false
self.scrollNode.view.canCancelContentTouches = true
self.scrollNode.view.showsVerticalScrollIndicator = false
self.scrollNode.view.showsHorizontalScrollIndicator = false
super.init()
self.view.addSubview(self.centerDimView)
self.view.addSubview(self.topDimView)
self.view.addSubview(self.bottomDimView)
self.view.addSubview(self.trailingDimView)
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
self.clippingNode.view.addSubview(self.backgroundEffectView)
self.clippingNode.addSubnode(self.scrollNode)
self.addSubnode(self.clippingNode)
}
func setOverlayNode(_ overlayNode: ActionSheetGroupOverlayNode?) {
guard self.overlayNode == nil else {
return
}
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
overlayNode?.alpha = 0.0
self.overlayNode = overlayNode
if let overlayNode = overlayNode {
transition.updateAlpha(node: overlayNode, alpha: 1.0)
self.clippingNode.addSubnode(overlayNode)
} else if let overlayNode = self.overlayNode {
overlayNode.removeFromSupernode()
}
if let size = self.validLayout, let overlayNode = overlayNode {
overlayNode.updateLayout(size: size, transition: .immediate)
}
for node in self.itemNodes {
transition.updateAlpha(node: node, alpha: 0.0)
}
}
func updateItemNodes(_ nodes: [ActionSheetItemNode], leadingVisibleNodeCount: CGFloat = 1000.0) {
for node in self.itemNodes {
if !nodes.contains(where: { $0 === node }) {
node.removeFromSupernode()
}
}
for node in nodes {
if !self.itemNodes.contains(where: { $0 === node }) {
self.scrollNode.addSubnode(node)
}
}
self.itemNodes = nodes
self.leadingVisibleNodeCount = leadingVisibleNodeCount
self.invalidateCalculatedLayout()
}
func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var itemNodesHeight: CGFloat = 0.0
var leadingVisibleNodeSize: CGFloat = 0.0
var i = 0
var previousHadSeparator = false
for node in self.itemNodes {
if CGFloat(0.0).isLess(than: itemNodesHeight), previousHadSeparator {
itemNodesHeight += UIScreenPixel
}
previousHadSeparator = node.hasSeparator
let nodeSize = node.updateLayout(constrainedSize: CGSize(width: constrainedSize.width, height: constrainedSize.height - itemNodesHeight), transition: transition)
node.frame = CGRect(origin: CGPoint(x: 0.0, y: itemNodesHeight), size: nodeSize)
itemNodesHeight += nodeSize.height
if CGFloat(i).isLessThanOrEqualTo(leadingVisibleNodeCount) {
if CGFloat(0.0).isLess(than: leadingVisibleNodeSize), node.hasSeparator {
leadingVisibleNodeSize += UIScreenPixel
}
let factor: CGFloat = min(1.0, leadingVisibleNodeCount - CGFloat(i))
leadingVisibleNodeSize += nodeSize.height * factor
}
i += 1
}
let size = CGSize(width: constrainedSize.width, height: min(floorToScreenPixels(itemNodesHeight), constrainedSize.height))
self.validLayout = size
let scrollViewFrame = CGRect(origin: CGPoint(), size: size)
var updateOffset = false
if !self.scrollNode.frame.equalTo(scrollViewFrame) {
self.scrollNode.frame = scrollViewFrame
updateOffset = true
}
let backgroundEffectViewFrame = CGRect(origin: CGPoint(), size: size)
if !self.backgroundEffectView.frame.equalTo(backgroundEffectViewFrame) {
transition.updateFrame(view: self.backgroundEffectView, frame: backgroundEffectViewFrame)
}
let scrollViewContentSize = CGSize(width: size.width, height: itemNodesHeight)
if !self.scrollNode.view.contentSize.equalTo(scrollViewContentSize) {
self.scrollNode.view.contentSize = scrollViewContentSize
}
let scrollViewContentInsets = UIEdgeInsets(top: max(0.0, size.height - leadingVisibleNodeSize), left: 0.0, bottom: 0.0, right: 0.0)
if self.scrollNode.view.contentInset != scrollViewContentInsets {
self.scrollNode.view.contentInset = scrollViewContentInsets
}
if updateOffset {
self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: -scrollViewContentInsets.top)
}
if let overlayNode = self.overlayNode {
overlayNode.updateLayout(size: size, transition: transition)
}
self.updateOverscroll(size: size, transition: transition)
return size
}
private func currentVerticalOverscroll() -> CGFloat {
var verticalOverscroll: CGFloat = 0.0
if scrollNode.view.contentOffset.y < 0.0 {
verticalOverscroll = scrollNode.view.contentOffset.y
} else if scrollNode.view.contentOffset.y > scrollNode.view.contentSize.height - scrollNode.view.bounds.size.height {
verticalOverscroll = scrollNode.view.contentOffset.y - (scrollNode.view.contentSize.height - scrollNode.view.bounds.size.height)
}
return verticalOverscroll
}
private func currentRealVerticalOverscroll() -> CGFloat {
var verticalOverscroll: CGFloat = 0.0
if scrollNode.view.contentOffset.y < 0.0 {
verticalOverscroll = scrollNode.view.contentOffset.y
} else if scrollNode.view.contentOffset.y > scrollNode.view.contentSize.height - scrollNode.view.bounds.size.height {
verticalOverscroll = scrollNode.view.contentOffset.y - (scrollNode.view.contentSize.height - scrollNode.view.bounds.size.height)
}
return verticalOverscroll
}
private func updateOverscroll(size: CGSize, transition: ContainedViewLayoutTransition) {
let verticalOverscroll = self.currentVerticalOverscroll()
self.clippingNode.layer.sublayerTransform = CATransform3DMakeTranslation(0.0, min(0.0, verticalOverscroll), 0.0)
let clippingNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: max(0.0, -verticalOverscroll)), size: CGSize(width: size.width, height: size.height - abs(verticalOverscroll)))
if !self.clippingNode.frame.equalTo(clippingNodeFrame) {
transition.updateFrame(node: self.clippingNode, frame: clippingNodeFrame)
transition.updateFrame(view: self.centerDimView, frame: clippingNodeFrame)
transition.updateFrame(view: self.topDimView, frame: CGRect(x: 0.0, y: 0.0, width: clippingNodeFrame.size.width, height: max(0.0, clippingNodeFrame.minY)))
transition.updateFrame(view: self.bottomDimView, frame: CGRect(x: 0.0, y: clippingNodeFrame.maxY, width: clippingNodeFrame.size.width, height: max(0.0, self.bounds.size.height - clippingNodeFrame.maxY)))
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if let size = self.validLayout {
self.updateOverscroll(size: size, transition: .immediate)
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.clippingNode.frame.contains(point) {
return super.hitTest(point, with: event)
} else {
return nil
}
}
func animateDimViewsAlpha(from: CGFloat, to: CGFloat, duration: Double) {
for node in [self.centerDimView, self.topDimView, self.bottomDimView] {
node.layer.animateAlpha(from: from, to: to, duration: duration)
}
}
func itemNode(at index: Int) -> ActionSheetItemNode {
return self.itemNodes[index]
}
}
@@ -0,0 +1,189 @@
import UIKit
import AsyncDisplayKit
private let groupSpacing: CGFloat = 8.0
final class ActionSheetItemGroupsContainerNode: ASDisplayNode {
var theme: ActionSheetControllerTheme {
didSet {
self.setGroups(self.groups)
if let size = self.validSize {
let _ = self.updateLayout(constrainedSize: size, transition: .immediate)
}
}
}
private var highlightedItemIndex: Int? = nil
private var groups: [ActionSheetItemGroup] = []
var groupNodes: [ActionSheetItemGroupNode] = []
var requestLayout: (() -> Void)?
private var validSize: CGSize?
init(theme: ActionSheetControllerTheme) {
self.theme = theme
super.init()
}
func setHighlightedItemIndex(_ index: Int?, update: Bool = false) {
self.highlightedItemIndex = index
if update {
var groupIndex = 0
var i = 0
for _ in self.groups {
for itemNode in self.groupNodes[groupIndex].itemNodes {
if i == index {
itemNode.setHighlighted(true, animated: false)
} else {
itemNode.setHighlighted(false, animated: false)
}
i += 1
}
groupIndex += 1
}
}
}
func decreaseHighlightedIndex() {
let currentHighlightedIndex = self.highlightedItemIndex ?? 0
self.setHighlightedItemIndex(max(0, currentHighlightedIndex - 1), update: true)
}
func increaseHighlightedIndex() {
let currentHighlightedIndex = self.highlightedItemIndex ?? -1
var groupIndex = 0
var maxAvailabledIndex = 0
for _ in self.groups {
for _ in self.groupNodes[groupIndex].itemNodes {
maxAvailabledIndex += 1
}
groupIndex += 1
}
self.setHighlightedItemIndex(min(maxAvailabledIndex - 1, currentHighlightedIndex + 1), update: true)
}
func performHighlightedAction() {
guard let highlightedItemIndex = self.highlightedItemIndex else {
return
}
var i = 0
var groupIndex = 0
for _ in self.groups {
for itemNode in self.groupNodes[groupIndex].itemNodes {
if i == highlightedItemIndex {
itemNode.performAction()
return
}
i += 1
}
groupIndex += 1
}
}
func setGroups(_ groups: [ActionSheetItemGroup]) {
self.groups = groups
for groupNode in self.groupNodes {
groupNode.removeFromSupernode()
}
self.groupNodes.removeAll()
var i = 0
for group in groups {
let groupNode = ActionSheetItemGroupNode(theme: self.theme)
let itemNodes = group.items.map({ $0.node(theme: self.theme) })
for node in itemNodes {
node.requestLayout = { [weak self] in
self?.requestLayout?()
}
let index = i
node.highlightedUpdated = { [weak self] highlighted in
if highlighted {
self?.highlightedItemIndex = index
}
}
i += 1
}
groupNode.updateItemNodes(itemNodes, leadingVisibleNodeCount: group.leadingVisibleNodeCount ?? 1000.0)
self.groupNodes.append(groupNode)
self.addSubnode(groupNode)
}
}
func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
self.validSize = constrainedSize
var groupsHeight: CGFloat = 0.0
var calculatedSizes: [CGSize] = []
for groupNode in self.groupNodes.reversed() {
if CGFloat(0.0).isLess(than: groupsHeight) {
groupsHeight += groupSpacing
}
let size = groupNode.updateLayout(constrainedSize: CGSize(width: constrainedSize.width, height: max(0.0, constrainedSize.height - groupsHeight)), transition: transition)
calculatedSizes.insert(size, at: 0)
groupsHeight += size.height
}
var itemGroupsHeight: CGFloat = 0.0
for i in 0 ..< self.groupNodes.count {
let groupNode = self.groupNodes[i]
let size = calculatedSizes[i]
if i != 0 {
itemGroupsHeight += groupSpacing
transition.updateFrame(view: self.groupNodes[i - 1].trailingDimView, frame: CGRect(x: 0.0, y: groupNodes[i - 1].bounds.size.height, width: size.width, height: groupSpacing))
}
transition.updateFrame(node: groupNode, frame: CGRect(origin: CGPoint(x: 0.0, y: itemGroupsHeight), size: size))
transition.updateFrame(view: groupNode.trailingDimView, frame: CGRect())
itemGroupsHeight += size.height
}
return CGSize(width: constrainedSize.width, height: min(groupsHeight, constrainedSize.height))
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard self.isUserInteractionEnabled else {
return nil
}
for groupNode in self.groupNodes {
if groupNode.frame.contains(point) {
return groupNode.hitTest(self.convert(point, to: groupNode), with: event)
}
}
return nil
}
func animateDimViewsAlpha(from: CGFloat, to: CGFloat, duration: Double) {
for node in self.groupNodes {
node.animateDimViewsAlpha(from: from, to: to, duration: duration)
}
}
public func updateItem(groupIndex: Int, itemIndex: Int, _ f: (ActionSheetItem) -> ActionSheetItem) {
var item = self.groups[groupIndex].items[itemIndex]
let itemNode = self.groupNodes[groupIndex].itemNode(at: itemIndex)
item = f(item)
item.updateNode(itemNode)
var groupItems = self.groups[groupIndex].items
groupItems[itemIndex] = item
self.groups[groupIndex] = ActionSheetItemGroup(items: groupItems)
}
func setItemGroupOverlayNode(groupIndex: Int, node: ActionSheetGroupOverlayNode) {
self.groupNodes[groupIndex].setOverlayNode(node)
}
}
@@ -0,0 +1,60 @@
import UIKit
import AsyncDisplayKit
open class ActionSheetItemNode: ASDisplayNode {
private let theme: ActionSheetControllerTheme
public let backgroundNode: ASDisplayNode
private let overflowSeparatorNode: ASDisplayNode
public var hasSeparator = true
public var requestLayout: (() -> Void)?
private var validSize: CGSize?
var highlightedUpdated: (Bool) -> Void = { _ in }
public init(theme: ActionSheetControllerTheme) {
self.theme = theme
self.backgroundNode = ASDisplayNode()
self.backgroundNode.backgroundColor = self.theme.itemBackgroundColor
self.overflowSeparatorNode = ASDisplayNode()
self.overflowSeparatorNode.backgroundColor = self.theme.itemHighlightedBackgroundColor
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.overflowSeparatorNode)
}
open func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let size = CGSize(width: constrainedSize.width, height: 57.0)
self.updateInternalLayout(size, constrainedSize: constrainedSize)
return size
}
func setHighlighted(_ highlighted: Bool, animated: Bool) {
}
func performAction() {
}
public func updateInternalLayout(_ calculatedSize: CGSize, constrainedSize: CGSize) {
self.validSize = constrainedSize
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: calculatedSize)
self.overflowSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: calculatedSize.height), size: CGSize(width: calculatedSize.width, height: UIScreenPixel))
self.overflowSeparatorNode.isHidden = !self.hasSeparator
}
public func requestLayoutUpdate() {
if let size = self.validSize {
let _ = self.updateLayout(constrainedSize: size, transition: .immediate)
}
}
}
@@ -0,0 +1,133 @@
import Foundation
import UIKit
import AsyncDisplayKit
public class ActionSheetSwitchItem: ActionSheetItem {
public let title: String
public let isOn: Bool
public let action: (Bool) -> Void
public init(title: String, isOn: Bool, action: @escaping (Bool) -> Void) {
self.title = title
self.isOn = isOn
self.action = action
}
public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
let node = ActionSheetSwitchNode(theme: theme)
node.setItem(self)
return node
}
public func updateNode(_ node: ActionSheetItemNode) {
guard let node = node as? ActionSheetSwitchNode else {
assertionFailure()
return
}
node.setItem(self)
node.requestLayoutUpdate()
}
}
public class ActionSheetSwitchNode: ActionSheetItemNode {
private let theme: ActionSheetControllerTheme
private var item: ActionSheetSwitchItem?
private let button: HighlightTrackingButton
private let label: ImmediateTextNode
private let switchNode: SwitchNode
private let accessibilityArea: AccessibilityAreaNode
override public init(theme: ActionSheetControllerTheme) {
self.theme = theme
self.button = HighlightTrackingButton()
self.button.isAccessibilityElement = false
self.label = ImmediateTextNode()
self.label.isUserInteractionEnabled = false
self.label.maximumNumberOfLines = 1
self.label.displaysAsynchronously = false
self.label.truncationType = .end
self.label.isAccessibilityElement = false
self.switchNode = SwitchNode()
self.switchNode.frameColor = theme.switchFrameColor
self.switchNode.contentColor = theme.switchContentColor
self.switchNode.handleColor = theme.switchHandleColor
self.switchNode.isAccessibilityElement = false
self.accessibilityArea = AccessibilityAreaNode()
super.init(theme: theme)
self.view.addSubview(self.button)
self.label.isUserInteractionEnabled = false
self.addSubnode(self.label)
self.addSubnode(self.switchNode)
self.addSubnode(self.accessibilityArea)
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
self.switchNode.valueUpdated = { [weak self] value in
self?.item?.action(value)
}
self.accessibilityArea.activate = { [weak self] in
self?.buttonPressed()
return true
}
}
func setItem(_ item: ActionSheetSwitchItem) {
self.item = item
let defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0))
self.label.attributedText = NSAttributedString(string: item.title, font: defaultFont, textColor: self.theme.primaryTextColor)
self.label.isAccessibilityElement = false
self.switchNode.isOn = item.isOn
self.accessibilityArea.accessibilityLabel = item.title
var accessibilityTraits: UIAccessibilityTraits = [.button]
if item.isOn {
accessibilityTraits.insert(.selected)
}
self.accessibilityArea.accessibilityTraits = accessibilityTraits
}
public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let size = CGSize(width: constrainedSize.width, height: 57.0)
self.button.frame = CGRect(origin: CGPoint(), size: size)
let labelSize = self.label.updateLayout(CGSize(width: max(1.0, size.width - 51.0 - 16.0 * 2.0), height: size.height))
self.label.frame = CGRect(origin: CGPoint(x: 16.0, y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize)
var switchSize = CGSize(width: 51.0, height: 31.0)
if let switchView = self.switchNode.view as? UISwitch {
if self.switchNode.bounds.size.width.isZero {
switchView.sizeToFit()
}
switchSize = switchView.bounds.size
}
self.switchNode.frame = CGRect(origin: CGPoint(x: size.width - 16.0 - switchSize.width, y: floor((size.height - switchSize.height) / 2.0)), size: switchSize)
self.accessibilityArea.frame = CGRect(origin: CGPoint(), size: size)
self.updateInternalLayout(size, constrainedSize: constrainedSize)
return size
}
@objc func buttonPressed() {
let value = !self.switchNode.isOn
self.switchNode.setOn(value, animated: true)
self.item?.action(value)
}
}
@@ -0,0 +1,110 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Markdown
public class ActionSheetTextItem: ActionSheetItem {
public enum Font {
case `default`
case large
}
public let title: String
public let font: Font
public let parseMarkdown: Bool
public init(title: String, font: Font = .default, parseMarkdown: Bool = true) {
self.title = title
self.font = font
self.parseMarkdown = parseMarkdown
}
public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
let node = ActionSheetTextNode(theme: theme)
node.setItem(self)
return node
}
public func updateNode(_ node: ActionSheetItemNode) {
guard let node = node as? ActionSheetTextNode else {
assertionFailure()
return
}
node.setItem(self)
node.requestLayoutUpdate()
}
}
public class ActionSheetTextNode: ActionSheetItemNode {
private let theme: ActionSheetControllerTheme
private var item: ActionSheetTextItem?
private let label: ImmediateTextNode
private let accessibilityArea: AccessibilityAreaNode
override public init(theme: ActionSheetControllerTheme) {
self.theme = theme
self.label = ImmediateTextNode()
self.label.isUserInteractionEnabled = false
self.label.maximumNumberOfLines = 0
self.label.displaysAsynchronously = false
self.label.truncationType = .end
self.label.isAccessibilityElement = false
self.label.textAlignment = .center
self.accessibilityArea = AccessibilityAreaNode()
self.accessibilityArea.accessibilityTraits = .staticText
super.init(theme: theme)
self.label.isUserInteractionEnabled = false
self.addSubnode(self.label)
self.addSubnode(self.accessibilityArea)
}
func setItem(_ item: ActionSheetTextItem) {
self.item = item
let fontSize: CGFloat
switch item.font {
case .default:
fontSize = 13.0
case .large:
fontSize = 15.0
}
let defaultFont = Font.regular(floor(self.theme.baseFontSize * fontSize / 17.0))
let boldFont = Font.semibold(floor(self.theme.baseFontSize * fontSize / 17.0))
if item.parseMarkdown {
let body = MarkdownAttributeSet(font: defaultFont, textColor: self.theme.secondaryTextColor)
let bold = MarkdownAttributeSet(font: boldFont, textColor: self.theme.secondaryTextColor)
let link = body
self.label.attributedText = parseMarkdownIntoAttributedString(item.title, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in
return nil
}))
} else {
self.label.attributedText = NSAttributedString(string: item.title, font: defaultFont, textColor: self.theme.secondaryTextColor, paragraphAlignment: .center)
}
self.accessibilityArea.accessibilityLabel = item.title
}
public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let labelSize = self.label.updateLayout(CGSize(width: max(1.0, constrainedSize.width - 20.0), height: constrainedSize.height))
let size = CGSize(width: constrainedSize.width, height: max(57.0, labelSize.height + 32.0))
self.label.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - labelSize.width) / 2.0), y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize)
self.accessibilityArea.frame = CGRect(origin: CGPoint(), size: size)
self.updateInternalLayout(size, constrainedSize: constrainedSize)
return size
}
}
@@ -0,0 +1,92 @@
import Foundation
import UIKit
public enum ActionSheetControllerThemeBackgroundType {
case light
case dark
}
public final class ActionSheetControllerTheme: Equatable {
public let dimColor: UIColor
public let backgroundType: ActionSheetControllerThemeBackgroundType
public let itemBackgroundColor: UIColor
public let itemHighlightedBackgroundColor: UIColor
public let standardActionTextColor: UIColor
public let destructiveActionTextColor: UIColor
public let disabledActionTextColor: UIColor
public let primaryTextColor: UIColor
public let secondaryTextColor: UIColor
public let controlAccentColor: UIColor
public let controlColor: UIColor
public let switchFrameColor: UIColor
public let switchContentColor: UIColor
public let switchHandleColor: UIColor
public let baseFontSize: CGFloat
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, baseFontSize: CGFloat) {
self.dimColor = dimColor
self.backgroundType = backgroundType
self.itemBackgroundColor = itemBackgroundColor
self.itemHighlightedBackgroundColor = itemHighlightedBackgroundColor
self.standardActionTextColor = standardActionTextColor
self.destructiveActionTextColor = destructiveActionTextColor
self.disabledActionTextColor = disabledActionTextColor
self.primaryTextColor = primaryTextColor
self.secondaryTextColor = secondaryTextColor
self.controlAccentColor = controlAccentColor
self.controlColor = controlColor
self.switchFrameColor = switchFrameColor
self.switchContentColor = switchContentColor
self.switchHandleColor = switchHandleColor
self.baseFontSize = min(26.0, baseFontSize)
}
public static func ==(lhs: ActionSheetControllerTheme, rhs: ActionSheetControllerTheme) -> Bool {
if lhs.dimColor != rhs.dimColor {
return false
}
if lhs.backgroundType != rhs.backgroundType {
return false
}
if lhs.itemBackgroundColor != rhs.itemBackgroundColor {
return false
}
if lhs.itemHighlightedBackgroundColor != rhs.itemHighlightedBackgroundColor {
return false
}
if lhs.standardActionTextColor != rhs.standardActionTextColor {
return false
}
if lhs.destructiveActionTextColor != rhs.destructiveActionTextColor {
return false
}
if lhs.disabledActionTextColor != rhs.disabledActionTextColor {
return false
}
if lhs.primaryTextColor != rhs.primaryTextColor {
return false
}
if lhs.secondaryTextColor != rhs.secondaryTextColor {
return false
}
if lhs.controlAccentColor != rhs.controlAccentColor {
return false
}
if lhs.controlColor != rhs.controlColor {
return false
}
if lhs.switchFrameColor != rhs.switchFrameColor {
return false
}
if lhs.switchContentColor != rhs.switchContentColor {
return false
}
if lhs.switchHandleColor != rhs.switchHandleColor {
return false
}
if lhs.baseFontSize != rhs.baseFontSize {
return false
}
return true
}
}
@@ -0,0 +1,33 @@
import Foundation
import UIKit
import AsyncDisplayKit
open class AlertContentNode: ASDisplayNode {
open var requestLayout: ((ContainedViewLayoutTransition) -> Void)?
open var dismissOnOutsideTap: Bool {
return true
}
open func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
assertionFailure()
return CGSize()
}
open func updateTheme(_ theme: AlertControllerTheme) {
}
open func performHighlightedAction() {
}
open func decreaseHighlightedIndex() {
}
open func increaseHighlightedIndex() {
}
}
@@ -0,0 +1,198 @@
import Foundation
import UIKit
import AsyncDisplayKit
public enum AlertControllerThemeBackgroundType {
case light
case dark
}
public final class AlertControllerTheme: Equatable {
public let backgroundType: ActionSheetControllerThemeBackgroundType
public let backgroundColor: UIColor
public let separatorColor: UIColor
public let highlightedItemColor: UIColor
public let primaryColor: UIColor
public let secondaryColor: UIColor
public let accentColor: UIColor
public let contrastColor: UIColor
public let destructiveColor: UIColor
public let disabledColor: UIColor
public let controlBorderColor: UIColor
public let baseFontSize: CGFloat
public init(backgroundType: ActionSheetControllerThemeBackgroundType, backgroundColor: UIColor, separatorColor: UIColor, highlightedItemColor: UIColor, primaryColor: UIColor, secondaryColor: UIColor, accentColor: UIColor, contrastColor: UIColor, destructiveColor: UIColor, disabledColor: UIColor, controlBorderColor: UIColor, baseFontSize: CGFloat) {
self.backgroundType = backgroundType
self.backgroundColor = backgroundColor
self.separatorColor = separatorColor
self.highlightedItemColor = highlightedItemColor
self.primaryColor = primaryColor
self.secondaryColor = secondaryColor
self.accentColor = accentColor
self.contrastColor = contrastColor
self.destructiveColor = destructiveColor
self.disabledColor = disabledColor
self.controlBorderColor = controlBorderColor
self.baseFontSize = baseFontSize
}
public static func ==(lhs: AlertControllerTheme, rhs: AlertControllerTheme) -> Bool {
if lhs.backgroundType != rhs.backgroundType {
return false
}
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
if lhs.separatorColor != rhs.separatorColor {
return false
}
if lhs.highlightedItemColor != rhs.highlightedItemColor {
return false
}
if lhs.primaryColor != rhs.primaryColor {
return false
}
if lhs.secondaryColor != rhs.secondaryColor {
return false
}
if lhs.accentColor != rhs.accentColor {
return false
}
if lhs.destructiveColor != rhs.destructiveColor {
return false
}
if lhs.disabledColor != rhs.disabledColor {
return false
}
if lhs.baseFontSize != rhs.baseFontSize {
return false
}
return true
}
}
open class AlertController: ViewController, StandalonePresentableController, KeyShortcutResponder {
private var controllerNode: AlertControllerNode {
return self.displayNode as! AlertControllerNode
}
public var theme: AlertControllerTheme {
didSet {
if oldValue != self.theme {
self.controllerNode.updateTheme(self.theme)
}
}
}
public let contentNode: AlertContentNode
private let allowInputInset: Bool
private weak var existingAlertController: AlertController?
public var willDismiss: (() -> Void)?
public var dismissed: ((Bool) -> Void)?
public init(theme: AlertControllerTheme, contentNode: AlertContentNode, existingAlertController: AlertController? = nil, allowInputInset: Bool = true) {
self.theme = theme
self.contentNode = contentNode
self.existingAlertController = existingAlertController
self.allowInputInset = allowInputInset
super.init(navigationBarPresentationData: nil)
self.blocksBackgroundWhenInOverlay = true
self.statusBar.statusBarStyle = .Ignore
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var isDismissed = false
override open func loadDisplayNode() {
self.displayNode = AlertControllerNode(contentNode: self.contentNode, theme: self.theme, allowInputInset: self.allowInputInset)
self.displayNodeDidLoad()
self.controllerNode.existingAlertControllerNode = self.existingAlertController?.controllerNode
self.controllerNode.dismiss = { [weak self] in
if let strongSelf = self, strongSelf.contentNode.dismissOnOutsideTap {
strongSelf.willDismiss?()
strongSelf.controllerNode.animateOut {
self?.dismissed?(true)
self?.isDismissed = true
self?.dismiss()
}
}
}
}
override open func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.existingAlertController?.dismiss(completion: nil)
self.existingAlertController = nil
self.controllerNode.animateIn()
}
override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, transition: transition)
}
override open func dismiss(completion: (() -> Void)? = nil) {
if !self.isDismissed {
self.isDismissed = true
self.dismissed?(false)
}
self.presentingViewController?.dismiss(animated: false, completion: completion)
}
open func dismissAnimated() {
self.controllerNode.animateOut { [weak self] in
self?.dismiss()
}
}
public var keyShortcuts: [KeyShortcut] {
return [
KeyShortcut(
input: UIKeyCommand.inputEscape,
modifiers: [],
action: { [weak self] in
self?.dismissAnimated()
}
),
KeyShortcut(
input: "W",
modifiers: [.command],
action: { [weak self] in
self?.dismissAnimated()
}
),
KeyShortcut(
input: "\r",
modifiers: [],
action: { [weak self] in
self?.controllerNode.performHighlightedAction()
}
),
KeyShortcut(
input: UIKeyCommand.inputUpArrow,
modifiers: [],
action: { [weak self] in
self?.controllerNode.decreaseHighlightedIndex()
}
),
KeyShortcut(
input: UIKeyCommand.inputDownArrow,
modifiers: [],
action: { [weak self] in
self?.controllerNode.increaseHighlightedIndex()
}
)
]
}
}
@@ -0,0 +1,204 @@
import Foundation
import UIKit
import AsyncDisplayKit
final class AlertControllerNode: ASDisplayNode {
var existingAlertControllerNode: AlertControllerNode?
private let dimContainerView: UIView
private let centerDimView: UIImageView
private let topDimView: UIView
private let bottomDimView: UIView
private let leftDimView: UIView
private let rightDimView: UIView
private let containerNode: ASDisplayNode
// private let effectNode: ASDisplayNode
private let effectView: UIVisualEffectView
private let backgroundNode: ASDisplayNode
private let contentNode: AlertContentNode
private let allowInputInset: Bool
private var containerLayout: ContainerViewLayout?
var dismiss: (() -> Void)?
init(contentNode: AlertContentNode, theme: AlertControllerTheme, allowInputInset: Bool) {
self.allowInputInset = allowInputInset
let dimColor = UIColor(white: 0.0, alpha: 0.5)
self.dimContainerView = UIView()
self.centerDimView = UIImageView()
self.centerDimView.backgroundColor = dimColor
self.topDimView = UIView()
self.topDimView.backgroundColor = dimColor
self.bottomDimView = UIView()
self.bottomDimView.backgroundColor = dimColor
self.leftDimView = UIView()
self.leftDimView.backgroundColor = dimColor
self.rightDimView = UIView()
self.rightDimView.backgroundColor = dimColor
self.containerNode = ASDisplayNode()
self.containerNode.layer.cornerRadius = 14.0
self.containerNode.layer.masksToBounds = true
self.backgroundNode = ASDisplayNode()
self.backgroundNode.backgroundColor = theme.backgroundColor
// self.effectNode = ASDisplayNode(viewBlock: {
// return UIVisualEffectView(effect: UIBlurEffect(style: theme.backgroundType == .light ? .light : .dark))
// })
self.effectView = UIVisualEffectView(effect: UIBlurEffect(style: theme.backgroundType == .light ? .light : .dark))
self.contentNode = contentNode
super.init()
self.view.addSubview(self.dimContainerView)
self.dimContainerView.addSubview(self.centerDimView)
self.dimContainerView.addSubview(self.topDimView)
self.dimContainerView.addSubview(self.bottomDimView)
self.dimContainerView.addSubview(self.leftDimView)
self.dimContainerView.addSubview(self.rightDimView)
self.containerNode.view.addSubview(self.effectView)
// self.containerNode.addSubnode(self.effectNode)
self.containerNode.addSubnode(self.backgroundNode)
self.containerNode.addSubnode(self.contentNode)
self.addSubnode(self.containerNode)
self.contentNode.requestLayout = { [weak self] transition in
if let strongSelf = self, let containerLayout = self?.containerLayout {
strongSelf.containerLayoutUpdated(containerLayout, transition: transition)
}
}
}
override func didLoad() {
super.didLoad()
self.topDimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimmingNodeTapGesture(_:))))
self.bottomDimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimmingNodeTapGesture(_:))))
self.leftDimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimmingNodeTapGesture(_:))))
self.rightDimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimmingNodeTapGesture(_:))))
}
func performHighlightedAction() {
self.contentNode.performHighlightedAction()
}
func decreaseHighlightedIndex() {
self.contentNode.decreaseHighlightedIndex()
}
func increaseHighlightedIndex() {
self.contentNode.increaseHighlightedIndex()
}
func updateTheme(_ theme: AlertControllerTheme) {
self.effectView.effect = UIBlurEffect(style: theme.backgroundType == .light ? .light : .dark)
self.backgroundNode.backgroundColor = theme.backgroundColor
self.contentNode.updateTheme(theme)
}
func animateIn() {
if let previousNode = self.existingAlertControllerNode {
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .spring)
previousNode.position = previousNode.position.offsetBy(dx: -previousNode.frame.width, dy: 0.0)
self.addSubnode(previousNode)
let position = self.position
self.position = position.offsetBy(dx: self.frame.width, dy: 0.0)
transition.animateView {
self.position = position
} completion: { _ in
previousNode.removeFromSupernode()
}
self.existingAlertControllerNode = nil
} else {
self.centerDimView.backgroundColor = nil
self.centerDimView.image = generateStretchableFilledCircleImage(radius: 16.0, color: nil, backgroundColor: UIColor(white: 0.0, alpha: 0.5))
self.centerDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.topDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.bottomDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.leftDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.rightDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)/*, completion: { [weak self] finished in
if finished {
self?.centerDimView.backgroundColor = nil
self?.centerDimView.image = generateStretchableFilledCircleImage(radius: 16.0, color: nil, backgroundColor: UIColor(white: 0.0, alpha: 0.5))
}
})*/
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)
self.dimContainerView.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, removeOnCompletion: true, additive: false, completion: nil)
}
}
func animateOut(completion: @escaping () -> Void) {
self.containerNode.layer.removeAllAnimations()
//self.centerDimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
//self.centerDimView.image = nil
self.centerDimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
self.topDimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
self.bottomDimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
self.leftDimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
self.rightDimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
self.containerNode.layer.animateScale(from: 1.0, to: 0.8, duration: 0.4, removeOnCompletion: false, completion: { _ in
completion()
})
self.dimContainerView.layer.animateScale(from: 1.0, to: 0.8, duration: 0.4, removeOnCompletion: false)
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
self.containerLayout = layout
var insetOptions: ContainerViewLayoutInsetOptions = [.statusBar]
if self.allowInputInset {
insetOptions.insert(.input)
}
var insets = layout.insets(options: insetOptions)
let maxWidth = min(240.0, layout.size.width - 70.0)
insets.left = floor((layout.size.width - maxWidth) / 2.0)
insets.right = floor((layout.size.width - maxWidth) / 2.0)
let contentAvailableFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: layout.size.width - insets.right, height: layout.size.height - insets.top - insets.bottom))
let contentSize = self.contentNode.updateLayout(size: contentAvailableFrame.size, transition: transition)
let containerSize = CGSize(width: contentSize.width, height: contentSize.height)
let containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: contentAvailableFrame.minY + floor((contentAvailableFrame.size.height - containerSize.height) / 2.0)), size: containerSize)
let outerEdge: CGFloat = 100.0
transition.updateFrame(view: self.dimContainerView, frame: CGRect(origin: .zero, size: layout.size))
transition.updateFrame(view: self.centerDimView, frame: containerFrame)
transition.updateFrame(view: self.topDimView, frame: CGRect(origin: CGPoint(x: -outerEdge, y: -outerEdge), size: CGSize(width: layout.size.width + outerEdge * 2.0, height: containerFrame.minY + outerEdge)))
transition.updateFrame(view: self.bottomDimView, frame: CGRect(origin: CGPoint(x: -outerEdge, y: containerFrame.maxY), size: CGSize(width: layout.size.width + outerEdge * 2.0, height: layout.size.height - containerFrame.maxY + outerEdge)))
transition.updateFrame(view: self.leftDimView, frame: CGRect(origin: CGPoint(x: -outerEdge, y: containerFrame.minY), size: CGSize(width: containerFrame.minX + outerEdge, height: containerFrame.height)))
transition.updateFrame(view: self.rightDimView, frame: CGRect(origin: CGPoint(x: containerFrame.maxX, y: containerFrame.minY), size: CGSize(width: layout.size.width - containerFrame.maxX + outerEdge, height: containerFrame.height)))
transition.updateFrame(node: self.containerNode, frame: containerFrame)
transition.animateView {
self.effectView.frame = CGRect(origin: CGPoint(), size: containerFrame.size)
}
// transition.updateFrame(view: self.effectView, frame: CGRect(origin: CGPoint(), size: containerFrame.size))
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: containerFrame.size))
transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: containerFrame.size))
}
@objc func dimmingNodeTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.dismiss?()
}
}
}
@@ -0,0 +1,548 @@
import UIKit
import UIKitRuntimeUtils
@objc private class CALayerAnimationDelegate: NSObject, CAAnimationDelegate {
private let keyPath: String?
var completion: ((Bool) -> Void)?
init(animation: CAAnimation, completion: ((Bool) -> Void)?) {
if let animation = animation as? CABasicAnimation {
self.keyPath = animation.keyPath
} else {
self.keyPath = nil
}
self.completion = completion
super.init()
}
@objc func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
if let anim = anim as? CABasicAnimation {
if anim.keyPath != self.keyPath {
return
}
}
if let completion = self.completion {
completion(flag)
self.completion = nil
}
}
}
private let completionKey = "CAAnimationUtils_completion"
public let kCAMediaTimingFunctionSpring = "CAAnimationUtilsSpringCurve"
public let kCAMediaTimingFunctionCustomSpringPrefix = "CAAnimationUtilsSpringCustomCurve"
public extension CAAnimation {
var completion: ((Bool) -> Void)? {
get {
if let delegate = self.delegate as? CALayerAnimationDelegate {
return delegate.completion
} else {
return nil
}
} set(value) {
if let delegate = self.delegate as? CALayerAnimationDelegate {
delegate.completion = value
} else {
self.delegate = CALayerAnimationDelegate(animation: self, completion: value)
}
}
}
}
private func adjustFrameRate(animation: CAAnimation) {
if #available(iOS 15.0, *) {
let maxFps = Float(UIScreen.main.maximumFramesPerSecond)
if maxFps > 61.0 {
var preferredFps: Float = maxFps
if let animation = animation as? CABasicAnimation {
if animation.keyPath == "opacity" {
preferredFps = 60.0
return
}
}
animation.preferredFrameRateRange = CAFrameRateRange(minimum: 30.0, maximum: preferredFps, preferred: maxFps)
}
}
}
public extension CALayer {
func makeAnimation(from: Any?, to: Any, keyPath: String, timingFunction: String, duration: Double, delay: Double = 0.0, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) -> CAAnimation {
if timingFunction.hasPrefix(kCAMediaTimingFunctionCustomSpringPrefix) {
let components = timingFunction.components(separatedBy: "_")
let damping = Float(components[1]) ?? 100.0
let initialVelocity = Float(components[2]) ?? 0.0
let animation = CASpringAnimation(keyPath: keyPath)
animation.fromValue = from
animation.toValue = to
animation.isRemovedOnCompletion = removeOnCompletion
animation.fillMode = .forwards
if let completion = completion {
animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion)
}
animation.damping = CGFloat(damping)
animation.initialVelocity = CGFloat(initialVelocity)
animation.mass = 5.0
animation.stiffness = 900.0
animation.duration = animation.settlingDuration
animation.timingFunction = CAMediaTimingFunction.init(name: .linear)
let k = Float(UIView.animationDurationFactor())
var speed: Float = 1.0
if k != 0 && k != 1 {
speed = Float(1.0) / k
}
animation.speed = speed * Float(animation.duration / duration)
animation.isAdditive = additive
if !delay.isZero {
animation.beginTime = self.convertTime(CACurrentMediaTime(), from: nil) + delay * UIView.animationDurationFactor()
animation.fillMode = .both
}
adjustFrameRate(animation: animation)
return animation
} else if timingFunction == kCAMediaTimingFunctionSpring {
if #available(iOS 26.0, *), abs(duration - 0.3832) <= 0.0001 {
let animation = make26SpringAnimationImpl(keyPath, duration)
animation.fromValue = from
animation.toValue = to
animation.isRemovedOnCompletion = removeOnCompletion
animation.fillMode = .forwards
if let completion {
animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion)
}
let k = Float(UIView.animationDurationFactor())
var speed: Float = 1.0
if k != 0 && k != 1 {
speed = Float(1.0) / k
}
animation.speed = speed * Float(animation.duration / duration)
animation.isAdditive = additive
if !delay.isZero {
animation.beginTime = self.convertTime(CACurrentMediaTime(), from: nil) + delay * UIView.animationDurationFactor()
animation.fillMode = .both
}
adjustFrameRate(animation: animation)
return animation
} else if duration == 0.5 {
let animation = makeSpringAnimation(keyPath, duration: duration)
animation.fromValue = from
animation.toValue = to
animation.isRemovedOnCompletion = removeOnCompletion
animation.fillMode = .forwards
if let completion = completion {
animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion)
}
let k = Float(UIView.animationDurationFactor())
var speed: Float = 1.0
if k != 0 && k != 1 {
speed = Float(1.0) / k
}
animation.speed = speed * Float(animation.duration / duration)
animation.isAdditive = additive
if !delay.isZero {
animation.beginTime = self.convertTime(CACurrentMediaTime(), from: nil) + delay * UIView.animationDurationFactor()
animation.fillMode = .both
}
adjustFrameRate(animation: animation)
return animation
} else {
let k = Float(UIView.animationDurationFactor())
var speed: Float = 1.0
if k != 0 && k != 1 {
speed = Float(1.0) / k
}
let animation = CABasicAnimation(keyPath: keyPath)
animation.fromValue = from
animation.toValue = to
animation.duration = duration
animation.timingFunction = CAMediaTimingFunction(controlPoints: 0.380, 0.700, 0.125, 1.000)
animation.isRemovedOnCompletion = removeOnCompletion
animation.fillMode = .forwards
animation.speed = speed
animation.isAdditive = additive
if let completion = completion {
animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion)
}
if !delay.isZero {
animation.beginTime = self.convertTime(CACurrentMediaTime(), from: nil) + delay * UIView.animationDurationFactor()
animation.fillMode = .both
}
adjustFrameRate(animation: animation)
return animation
}
} else {
let k = Float(UIView.animationDurationFactor())
var speed: Float = 1.0
if k != 0 && k != 1 {
speed = Float(1.0) / k
}
let animation = CABasicAnimation(keyPath: keyPath)
animation.fromValue = from
animation.toValue = to
animation.duration = duration
if let mediaTimingFunction = mediaTimingFunction {
animation.timingFunction = mediaTimingFunction
} else {
switch timingFunction {
case CAMediaTimingFunctionName.linear.rawValue, CAMediaTimingFunctionName.easeIn.rawValue, CAMediaTimingFunctionName.easeOut.rawValue, CAMediaTimingFunctionName.easeInEaseOut.rawValue, CAMediaTimingFunctionName.default.rawValue:
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName(rawValue: timingFunction))
default:
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
}
}
animation.isRemovedOnCompletion = removeOnCompletion
animation.fillMode = .forwards
animation.speed = speed
animation.isAdditive = additive
if let completion = completion {
animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion)
}
if !delay.isZero {
animation.beginTime = self.convertTime(CACurrentMediaTime(), from: nil) + delay * UIView.animationDurationFactor()
animation.fillMode = .both
}
adjustFrameRate(animation: animation)
return animation
}
}
func animate(from: Any?, to: Any, keyPath: String, timingFunction: String, duration: Double, delay: Double = 0.0, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil, key: String? = nil) {
let animation = self.makeAnimation(from: from, to: to, keyPath: keyPath, timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion)
self.add(animation, forKey: key ?? (additive ? nil : keyPath))
}
func animateGroup(_ animations: [CAAnimation], key: String, completion: ((Bool) -> Void)? = nil) {
let animationGroup = CAAnimationGroup()
var timeOffset = 0.0
for animation in animations {
animation.beginTime = self.convertTime(animation.beginTime, from: nil) + timeOffset
timeOffset += animation.duration / Double(animation.speed)
}
animationGroup.animations = animations
animationGroup.duration = timeOffset
if let completion = completion {
animationGroup.delegate = CALayerAnimationDelegate(animation: animationGroup, completion: completion)
}
adjustFrameRate(animation: animationGroup)
self.add(animationGroup, forKey: key)
}
func animateKeyframes(values: [AnyObject], keyTimes: [NSNumber]? = nil, duration: Double, keyPath: String, timingFunction: String = CAMediaTimingFunctionName.linear.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
let k = Float(UIView.animationDurationFactor())
var speed: Float = 1.0
if k != 0 && k != 1 {
speed = Float(1.0) / k
}
let animation = CAKeyframeAnimation(keyPath: keyPath)
animation.values = values
var effectiveKeyTimes: [NSNumber] = []
if let keyTimes {
effectiveKeyTimes = keyTimes
} else {
for i in 0 ..< values.count {
if i == 0 {
effectiveKeyTimes.append(0.0)
} else if i == values.count - 1 {
effectiveKeyTimes.append(1.0)
} else {
effectiveKeyTimes.append((Double(i) / Double(values.count - 1)) as NSNumber)
}
}
}
animation.keyTimes = effectiveKeyTimes
animation.speed = speed
animation.duration = duration
animation.isAdditive = additive
animation.calculationMode = .linear
if let mediaTimingFunction = mediaTimingFunction {
animation.timingFunction = mediaTimingFunction
} else {
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName(rawValue: timingFunction))
}
animation.isRemovedOnCompletion = removeOnCompletion
if let completion = completion {
animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion)
}
adjustFrameRate(animation: animation)
self.add(animation, forKey: keyPath)
}
func springAnimation(from: AnyObject, to: AnyObject, keyPath: String, duration: Double, delay: Double = 0.0, initialVelocity: CGFloat = 0.0, damping: CGFloat = 88.0, removeOnCompletion: Bool = true, additive: Bool = false) -> CABasicAnimation {
let animation = makeSpringBounceAnimation(keyPath, initialVelocity, damping)
animation.fromValue = from
animation.toValue = to
animation.isRemovedOnCompletion = removeOnCompletion
animation.fillMode = .forwards
let k = Float(UIView.animationDurationFactor())
var speed: Float = 1.0
if k != 0 && k != 1 {
speed = Float(1.0) / k
}
if !delay.isZero {
animation.beginTime = self.convertTime(CACurrentMediaTime(), from: nil) + delay * UIView.animationDurationFactor()
animation.fillMode = .both
}
animation.speed = speed * Float(animation.duration / duration)
animation.isAdditive = additive
adjustFrameRate(animation: animation)
return animation
}
func animateSpring(from: AnyObject, to: AnyObject, keyPath: String, duration: Double, delay: Double = 0.0, initialVelocity: CGFloat = 0.0, stiffness: CGFloat = 900.0, damping: CGFloat = 88.0, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
let animation = makeSpringBounceAnimation(keyPath, initialVelocity, damping)
animation.stiffness = stiffness
animation.fromValue = from
animation.toValue = to
animation.isRemovedOnCompletion = removeOnCompletion
animation.fillMode = .forwards
if let completion = completion {
animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion)
}
let k = Float(UIView.animationDurationFactor())
var speed: Float = 1.0
if k != 0 && k != 1 {
speed = Float(1.0) / k
}
if !delay.isZero {
animation.beginTime = self.convertTime(CACurrentMediaTime(), from: nil) + delay * UIView.animationDurationFactor()
animation.fillMode = .both
}
animation.speed = speed * Float(animation.duration / duration)
animation.isAdditive = additive
adjustFrameRate(animation: animation)
self.add(animation, forKey: additive ? nil : keyPath)
}
func animateAdditive(from: NSValue, to: NSValue, keyPath: String, key: String, timingFunction: String, mediaTimingFunction: CAMediaTimingFunction? = nil, duration: Double, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
let k = Float(UIView.animationDurationFactor())
var speed: Float = 1.0
if k != 0 && k != 1 {
speed = Float(1.0) / k
}
let animation = CABasicAnimation(keyPath: keyPath)
animation.fromValue = from
animation.toValue = to
animation.duration = duration
if let mediaTimingFunction = mediaTimingFunction {
animation.timingFunction = mediaTimingFunction
} else {
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName(rawValue: timingFunction))
}
animation.isRemovedOnCompletion = removeOnCompletion
animation.fillMode = .forwards
animation.speed = speed
animation.isAdditive = true
if let completion = completion {
animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion)
}
adjustFrameRate(animation: animation)
self.add(animation, forKey: key)
}
func animateAlpha(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> ())? = nil) {
self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "opacity", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: completion)
}
func animateScale(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "transform.scale", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion)
}
func animateSublayerScale(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "sublayerTransform.scale", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion)
}
func animateScaleX(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "transform.scale.x", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: completion)
}
func animateScaleY(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "transform.scale.y", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: completion)
}
func animateRotation(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "transform.rotation.z", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: completion)
}
func animatePosition(from: CGPoint, to: CGPoint, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, force: Bool = false, completion: ((Bool) -> Void)? = nil) {
if from == to && !force {
if let completion = completion {
completion(true)
}
return
}
self.animate(from: NSValue(cgPoint: from), to: NSValue(cgPoint: to), keyPath: "position", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion)
}
func animateAnchorPoint(from: CGPoint, to: CGPoint, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, force: Bool = false, completion: ((Bool) -> Void)? = nil) {
if from == to && !force {
if let completion = completion {
completion(true)
}
return
}
self.animate(from: NSValue(cgPoint: from), to: NSValue(cgPoint: to), keyPath: "anchorPoint", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion)
}
func animateBounds(from: CGRect, to: CGRect, duration: Double, delay: Double = 0.0, timingFunction: String, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, force: Bool = false, completion: ((Bool) -> Void)? = nil) {
if from == to && !force {
if let completion = completion {
completion(true)
}
return
}
self.animate(from: NSValue(cgRect: from), to: NSValue(cgRect: to), keyPath: "bounds", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion)
}
func animateWidth(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, force: Bool = false, completion: ((Bool) -> Void)? = nil) {
if from == to && !force {
if let completion = completion {
completion(true)
}
return
}
self.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.size.width", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion)
}
func animateHeight(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, force: Bool = false, completion: ((Bool) -> Void)? = nil) {
if from == to && !force {
if let completion = completion {
completion(true)
}
return
}
self.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.size.height", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion)
}
func animateBoundsOriginXAdditive(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
self.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.origin.x", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion)
}
func animateBoundsOriginYAdditive(from: CGFloat, to: CGFloat, duration: Double, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
self.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.origin.y", timingFunction: timingFunction, duration: duration, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion)
}
func animateBoundsOriginXAdditive(from: CGFloat, to: CGFloat, duration: Double, mediaTimingFunction: CAMediaTimingFunction) {
self.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.origin.x", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: duration, mediaTimingFunction: mediaTimingFunction, additive: true)
}
func animateBoundsOriginYAdditive(from: CGFloat, to: CGFloat, duration: Double, mediaTimingFunction: CAMediaTimingFunction) {
self.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.origin.y", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: duration, mediaTimingFunction: mediaTimingFunction, additive: true)
}
func animateBoundsOriginAdditive(from: CGPoint, to: CGPoint, duration: Double, mediaTimingFunction: CAMediaTimingFunction) {
self.animate(from: NSValue(cgPoint: from), to: NSValue(cgPoint: to), keyPath: "bounds.origin", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: duration, mediaTimingFunction: mediaTimingFunction, additive: true)
}
func animateBoundsOriginAdditive(from: CGPoint, to: CGPoint, duration: Double, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil) {
self.animate(from: NSValue(cgPoint: from), to: NSValue(cgPoint: to), keyPath: "bounds.origin", timingFunction: timingFunction, duration: duration, mediaTimingFunction: mediaTimingFunction, additive: true)
}
func animateShapeLineWidth(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "lineWidth", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion)
}
func animatePositionKeyframes(values: [CGPoint], duration: Double, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
self.animateKeyframes(values: values.map { NSValue(cgPoint: $0) }, duration: duration, keyPath: "position")
}
func animateFrame(from: CGRect, to: CGRect, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, force: Bool = false, completion: ((Bool) -> Void)? = nil) {
if from == to && !force {
if let completion = completion {
completion(true)
}
return
}
var interrupted = false
var completedPosition = false
var completedBounds = false
let partialCompletion: () -> Void = {
if interrupted || (completedPosition && completedBounds) {
if let completion = completion {
completion(!interrupted)
}
}
}
var fromPosition = CGPoint(x: from.midX, y: from.midY)
var toPosition = CGPoint(x: to.midX, y: to.midY)
var fromBounds = CGRect(origin: self.bounds.origin, size: from.size)
var toBounds = CGRect(origin: self.bounds.origin, size: to.size)
if additive {
fromPosition.x = -(toPosition.x - fromPosition.x)
fromPosition.y = -(toPosition.y - fromPosition.y)
toPosition = CGPoint()
fromBounds.size.width = -(toBounds.width - fromBounds.width)
fromBounds.size.height = -(toBounds.height - fromBounds.height)
toBounds = CGRect()
}
self.animatePosition(from: fromPosition, to: toPosition, duration: duration, delay: delay, timingFunction: timingFunction, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, force: force, completion: { value in
if !value {
interrupted = true
}
completedPosition = true
partialCompletion()
})
self.animateBounds(from: fromBounds, to: toBounds, duration: duration, delay: delay, timingFunction: timingFunction, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, force: force, completion: { value in
if !value {
interrupted = true
}
completedBounds = true
partialCompletion()
})
}
func cancelAnimationsRecursive(key: String) {
self.removeAnimation(forKey: key)
if let sublayers = self.sublayers {
for layer in sublayers {
layer.cancelAnimationsRecursive(key: key)
}
}
}
}
@@ -0,0 +1,133 @@
import Foundation
import UIKit
final class ChildWindowHostView: UIView, WindowHost {
var updateSize: ((CGSize) -> Void)?
var layoutSubviewsEvent: (() -> Void)?
var hitTestImpl: ((CGPoint, UIEvent?) -> UIView?)?
var presentController: ((ContainableController, PresentationSurfaceLevel, Bool, @escaping () -> Void) -> Void)?
var invalidateDeferScreenEdgeGestureImpl: (() -> Void)?
var invalidatePrefersOnScreenNavigationHiddenImpl: (() -> Void)?
var invalidateSupportedOrientationsImpl: (() -> Void)?
var cancelInteractiveKeyboardGesturesImpl: (() -> Void)?
var forEachControllerImpl: (((ContainableController) -> Void) -> Void)?
var getAccessibilityElementsImpl: (() -> [Any]?)?
override var frame: CGRect {
didSet {
if self.frame.size != oldValue.size {
self.updateSize?(self.frame.size)
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
self.layoutSubviewsEvent?()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return self.hitTestImpl?(point, event)
}
func invalidateDeferScreenEdgeGestures() {
self.invalidateDeferScreenEdgeGestureImpl?()
}
func invalidatePrefersOnScreenNavigationHidden() {
self.invalidatePrefersOnScreenNavigationHiddenImpl?()
}
func invalidateSupportedOrientations() {
self.invalidateSupportedOrientationsImpl?()
}
func cancelInteractiveKeyboardGestures() {
self.cancelInteractiveKeyboardGesturesImpl?()
}
func forEachController(_ f: (ContainableController) -> Void) {
self.forEachControllerImpl?(f)
}
func present(_ controller: ContainableController, on level: PresentationSurfaceLevel, blockInteraction: Bool, completion: @escaping () -> Void) {
self.presentController?(controller, level, blockInteraction, completion)
}
func presentInGlobalOverlay(_ controller: ContainableController) {
self.presentController?(controller, .root, true, {})
}
func addGlobalPortalHostView(sourceView: PortalSourceView) {
}
}
public func childWindowHostView(parent: UIView) -> WindowHostView {
let view = ChildWindowHostView()
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
let hostView = WindowHostView(containerView: view, eventView: view, isRotating: {
return false
}, systemUserInterfaceStyle: .single(.light), currentInterfaceOrientation: {
return .portrait
}, updateSupportedInterfaceOrientations: { orientations in
}, updateDeferScreenEdgeGestures: { edges in
}, updatePrefersOnScreenNavigationHidden: { value in
}, updateStatusBar: { _, _, _ in
})
view.updateSize = { [weak hostView] size in
hostView?.updateSize?(size, 0.0, .portrait)
}
view.layoutSubviewsEvent = { [weak hostView] in
hostView?.layoutSubviews?()
}
/*window.updateIsUpdatingOrientationLayout = { [weak hostView] value in
hostView?.isUpdatingOrientationLayout = value
}
window.updateToInterfaceOrientation = { [weak hostView] in
hostView?.updateToInterfaceOrientation?()
}*/
view.presentController = { [weak hostView] controller, level, block, f in
hostView?.present?(controller, level, block, f)
}
/*view.presentNativeImpl = { [weak hostView] controller in
hostView?.presentNative?(controller)
}*/
view.hitTestImpl = { [weak hostView] point, event in
return hostView?.hitTest?(point, event)
}
view.invalidateDeferScreenEdgeGestureImpl = { [weak hostView] in
hostView?.invalidateDeferScreenEdgeGesture?()
}
view.invalidatePrefersOnScreenNavigationHiddenImpl = { [weak hostView] in
hostView?.invalidatePrefersOnScreenNavigationHidden?()
}
view.invalidateSupportedOrientationsImpl = { [weak hostView] in
hostView?.invalidateSupportedOrientations?()
}
view.cancelInteractiveKeyboardGesturesImpl = { [weak hostView] in
hostView?.cancelInteractiveKeyboardGestures?()
}
view.forEachControllerImpl = { [weak hostView] f in
hostView?.forEachController?(f)
}
view.getAccessibilityElementsImpl = { [weak hostView] in
return hostView?.getAccessibilityElements?()
}
return hostView
}
@@ -0,0 +1,166 @@
import Foundation
import UIKit
import AsyncDisplayKit
private let titleFont = Font.bold(11.0)
public final class CollectionIndexNode: ASDisplayNode {
public static let searchIndex: String = "_$search$_"
private var currentSize: CGSize?
private var currentSections: [String] = []
private var currentColor: UIColor?
private var titleNodes: [String: (node: ImmediateTextNode, size: CGSize)] = [:]
private var scrollFeedback: HapticFeedback?
private var currentSelectedIndex: String?
public var indexSelected: ((String) -> Void)?
override public init() {
super.init()
}
override public func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))))
}
public func update(size: CGSize, color: UIColor, sections: [String], transition: ContainedViewLayoutTransition) {
if self.currentColor == nil || !color.isEqual(self.currentColor) {
self.currentColor = color
for (title, nodeAndSize) in self.titleNodes {
nodeAndSize.node.attributedText = NSAttributedString(string: title, font: titleFont, textColor: color)
let _ = nodeAndSize.node.updateLayout(CGSize(width: 100.0, height: 100.0))
}
}
if self.currentSize == size && self.currentSections == sections {
return
}
self.currentSize = size
self.currentSections = sections
let itemHeight: CGFloat = 15.0
let verticalInset: CGFloat = 10.0
let maxHeight = size.height - verticalInset * 2.0
let maxItemCount = min(sections.count, Int(floor(maxHeight / itemHeight)))
let skipCount: Int
if sections.isEmpty {
skipCount = 1
} else {
skipCount = Int(ceil(CGFloat(sections.count) / CGFloat(maxItemCount)))
}
let actualCount: CGFloat = ceil(CGFloat(sections.count) / CGFloat(skipCount))
let totalHeight = actualCount * itemHeight
let verticalOrigin = verticalInset + floor((maxHeight - totalHeight) / 2.0)
var validTitles = Set<String>()
var currentIndex = 0
var displayIndex = 0
var addedLastTitle = false
let addTitle: (Int) -> Void = { index in
let title = sections[index]
let nodeAndSize: (node: ImmediateTextNode, size: CGSize)
var animate = false
if let current = self.titleNodes[title] {
animate = true
nodeAndSize = current
} else {
let node = ImmediateTextNode()
node.attributedText = NSAttributedString(string: title, font: titleFont, textColor: color)
let nodeSize = node.updateLayout(CGSize(width: 100.0, height: 100.0))
nodeAndSize = (node, nodeSize)
self.addSubnode(node)
self.titleNodes[title] = nodeAndSize
}
validTitles.insert(title)
let previousPosition = nodeAndSize.node.position
nodeAndSize.node.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - nodeAndSize.size.width) / 2.0), y: verticalOrigin + itemHeight * CGFloat(displayIndex) + floor((itemHeight - nodeAndSize.size.height) / 2.0)), size: nodeAndSize.size)
if animate {
transition.animatePosition(node: nodeAndSize.node, from: previousPosition)
}
currentIndex += skipCount
displayIndex += 1
}
while currentIndex < sections.count {
if currentIndex == sections.count - 1 {
addedLastTitle = true
}
addTitle(currentIndex)
}
if !addedLastTitle && sections.count > 0 {
addTitle(sections.count - 1)
}
var removeTitles: [String] = []
for title in self.titleNodes.keys {
if !validTitles.contains(title) {
removeTitles.append(title)
}
}
for title in removeTitles {
self.titleNodes.removeValue(forKey: title)?.node.removeFromSupernode()
}
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.isUserInteractionEnabled, self.bounds.insetBy(dx: -5.0, dy: 0.0).contains(point) {
return self.view
} else {
return nil
}
}
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
var locationTitleAndPosition: (String, CGFloat)?
let location = recognizer.location(in: self.view)
for (title, nodeAndSize) in self.titleNodes {
let nodeFrame = nodeAndSize.node.frame
if location.y >= nodeFrame.minY - 5.0 && location.y <= nodeFrame.maxY + 5.0 {
if let currentTitleAndPosition = locationTitleAndPosition {
let distance = abs(nodeFrame.midY - location.y)
let previousDistance = abs(currentTitleAndPosition.1 - location.y)
if distance < previousDistance {
locationTitleAndPosition = (title, nodeFrame.midY)
}
} else {
locationTitleAndPosition = (title, nodeFrame.midY)
}
}
}
let locationTitle = locationTitleAndPosition?.0
switch recognizer.state {
case .began:
self.currentSelectedIndex = locationTitle
if let locationTitle = locationTitle {
self.indexSelected?(locationTitle)
}
case .changed:
if locationTitle != self.currentSelectedIndex {
self.currentSelectedIndex = locationTitle
if let locationTitle = locationTitle {
self.indexSelected?(locationTitle)
if self.scrollFeedback == nil {
self.scrollFeedback = HapticFeedback()
}
self.scrollFeedback?.tap()
}
}
case .cancelled, .ended:
self.currentSelectedIndex = nil
default:
break
}
}
}
@@ -0,0 +1,30 @@
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
public protocol PresentableController: AnyObject {
func viewDidAppear(completion: @escaping () -> Void)
}
public protocol ContainableController: AnyObject {
var view: UIView! { get }
var displayNode: ASDisplayNode { get }
var isViewLoaded: Bool { get }
var isOpaqueWhenInOverlay: Bool { get }
var blocksBackgroundWhenInOverlay: Bool { get }
var ready: Promise<Bool> { get }
var updateTransitionWhenPresentedAsModal: ((CGFloat, ContainedViewLayoutTransition) -> Void)? { get set }
func combinedSupportedOrientations(currentOrientationToLock: UIInterfaceOrientationMask) -> ViewControllerSupportedOrientations
var deferScreenEdgeGestures: UIRectEdge { get }
var prefersOnScreenNavigationHidden: Bool { get }
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition)
func updateToInterfaceOrientation(_ orientation: UIInterfaceOrientation)
func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize?
func viewWillAppear(_ animated: Bool)
func viewWillDisappear(_ animated: Bool)
func viewDidAppear(_ animated: Bool)
func viewDidDisappear(_ animated: Bool)
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,210 @@
import UIKit
public struct ContainerViewLayoutInsetOptions: OptionSet {
public let rawValue: Int
public init(rawValue: Int) {
self.rawValue = rawValue
}
public init() {
self.rawValue = 0
}
public static let statusBar = ContainerViewLayoutInsetOptions(rawValue: 1 << 0)
public static let input = ContainerViewLayoutInsetOptions(rawValue: 1 << 1)
}
public enum ContainerViewLayoutSizeClass {
case compact
case regular
}
public struct LayoutMetrics: Equatable {
public let widthClass: ContainerViewLayoutSizeClass
public let heightClass: ContainerViewLayoutSizeClass
public let orientation: UIInterfaceOrientation?
public init(widthClass: ContainerViewLayoutSizeClass, heightClass: ContainerViewLayoutSizeClass, orientation: UIInterfaceOrientation?) {
self.widthClass = widthClass
self.heightClass = heightClass
self.orientation = orientation
}
public init() {
self.widthClass = .compact
self.heightClass = .compact
self.orientation = nil
}
}
public extension LayoutMetrics {
var isTablet: Bool {
if case .regular = self.widthClass {
return true
} else {
return false
}
}
}
public enum LayoutOrientation {
case portrait
case landscape
}
public struct ContainerViewLayout: Equatable {
public var size: CGSize
public var metrics: LayoutMetrics
public var deviceMetrics: DeviceMetrics
public var intrinsicInsets: UIEdgeInsets
public var safeInsets: UIEdgeInsets
public var additionalInsets: UIEdgeInsets
public var statusBarHeight: CGFloat?
public var inputHeight: CGFloat?
public var inputHeightIsInteractivellyChanging: Bool
public var inVoiceOver: Bool
public init(size: CGSize, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, intrinsicInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, additionalInsets: UIEdgeInsets, statusBarHeight: CGFloat?, inputHeight: CGFloat?, inputHeightIsInteractivellyChanging: Bool, inVoiceOver: Bool) {
self.size = size
self.metrics = metrics
self.deviceMetrics = deviceMetrics
self.intrinsicInsets = intrinsicInsets
self.safeInsets = safeInsets
self.additionalInsets = additionalInsets
self.statusBarHeight = statusBarHeight
self.inputHeight = inputHeight
self.inputHeightIsInteractivellyChanging = inputHeightIsInteractivellyChanging
self.inVoiceOver = inVoiceOver
}
public func addedInsets(insets: UIEdgeInsets) -> ContainerViewLayout {
return ContainerViewLayout(size: self.size, metrics: self.metrics, deviceMetrics: self.deviceMetrics, 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, additionalInsets: self.additionalInsets, statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging, inVoiceOver: self.inVoiceOver)
}
public func withUpdatedSize(_ size: CGSize) -> ContainerViewLayout {
return ContainerViewLayout(size: size, metrics: self.metrics, deviceMetrics: self.deviceMetrics, intrinsicInsets: self.intrinsicInsets, safeInsets: self.safeInsets, additionalInsets: self.additionalInsets, statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging, inVoiceOver: self.inVoiceOver)
}
public func withUpdatedIntrinsicInsets(_ intrinsicInsets: UIEdgeInsets) -> ContainerViewLayout {
return ContainerViewLayout(size: self.size, metrics: self.metrics, deviceMetrics: self.deviceMetrics, intrinsicInsets: intrinsicInsets, safeInsets: self.safeInsets, additionalInsets: self.additionalInsets, statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging, inVoiceOver: self.inVoiceOver)
}
public func withUpdatedSafeInsets(_ safeInsets: UIEdgeInsets) -> ContainerViewLayout {
return ContainerViewLayout(size: self.size, metrics: self.metrics, deviceMetrics: self.deviceMetrics, intrinsicInsets: self.intrinsicInsets, safeInsets: safeInsets, additionalInsets: self.additionalInsets, statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging, inVoiceOver: self.inVoiceOver)
}
public func withUpdatedAdditionalInsets(_ additionalInsets: UIEdgeInsets) -> ContainerViewLayout {
return ContainerViewLayout(size: self.size, metrics: self.metrics, deviceMetrics: self.deviceMetrics, intrinsicInsets: self.intrinsicInsets, safeInsets: self.safeInsets, additionalInsets: additionalInsets, statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging, inVoiceOver: self.inVoiceOver)
}
public func withUpdatedInputHeight(_ inputHeight: CGFloat?) -> ContainerViewLayout {
return ContainerViewLayout(size: self.size, metrics: self.metrics, deviceMetrics: self.deviceMetrics, intrinsicInsets: self.intrinsicInsets, safeInsets: self.safeInsets, additionalInsets: self.additionalInsets, statusBarHeight: self.statusBarHeight, inputHeight: inputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging, inVoiceOver: self.inVoiceOver)
}
public func withUpdatedMetrics(_ metrics: LayoutMetrics) -> ContainerViewLayout {
return ContainerViewLayout(size: self.size, metrics: metrics, deviceMetrics: self.deviceMetrics, intrinsicInsets: self.intrinsicInsets, safeInsets: self.safeInsets, additionalInsets: self.additionalInsets, statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging, inVoiceOver: self.inVoiceOver)
}
}
public extension ContainerViewLayout {
func insets(options: ContainerViewLayoutInsetOptions) -> UIEdgeInsets {
var insets = self.intrinsicInsets
if let statusBarHeight = self.statusBarHeight, options.contains(.statusBar) {
insets.top = max(statusBarHeight, insets.top)
}
if let inputHeight = self.inputHeight, options.contains(.input) {
insets.bottom = max(inputHeight, insets.bottom)
}
return insets
}
var isModalOverlay: Bool {
if case .tablet = self.deviceMetrics.type {
if case .regular = self.metrics.widthClass {
return abs(max(self.size.width, self.size.height) - self.deviceMetrics.screenSize.height) > 1.0
}
}
return false
}
var isNonExclusive: Bool {
if case .tablet = self.deviceMetrics.type {
if case .compact = self.metrics.widthClass {
return true
}
if case .compact = self.metrics.heightClass {
return true
}
}
return false
}
var deviceOrientationSize: CGSize {
let screenSize = self.deviceMetrics.screenSize
return self.actualOrientation == .landscape ? CGSize(width: screenSize.height, height: screenSize.width) : screenSize
}
var inSplitView: Bool {
guard case .tablet = self.deviceMetrics.type else {
return false
}
guard self.metrics.widthClass == .compact || self.metrics.heightClass == .compact else {
return false
}
let orient = self.deviceOrientationSize
guard abs(self.size.height - orient.height) < 1.0 else {
return false
}
let ratio = self.size.width / max(orient.width, 1.0)
let tol: CGFloat = 0.04
let isSplitFraction = abs(ratio - 0.5) < tol || abs(ratio - (1.0/3.0)) < tol || abs(ratio - (2.0/3.0)) < tol
return isSplitFraction
}
var inSlideOver: Bool {
guard case .tablet = self.deviceMetrics.type else {
return false
}
guard self.metrics.widthClass == .compact || self.metrics.heightClass == .compact else {
return false
}
let currentLong = max(self.size.width, self.size.height)
let screenLong = max(self.deviceMetrics.screenSize.width, self.deviceMetrics.screenSize.height)
if abs(currentLong - screenLong) > 10.0 {
return true
}
return false
}
var actualOrientation: LayoutOrientation {
let screenPortraitHeight = max(self.deviceMetrics.screenSize.width, self.deviceMetrics.screenSize.height)
let screenPortraitWidth = min(self.deviceMetrics.screenSize.width, self.deviceMetrics.screenSize.height)
let deltaPortrait = abs(self.size.height - screenPortraitHeight)
let deltaLandscape = abs(self.size.height - screenPortraitWidth)
return deltaLandscape < deltaPortrait ? .landscape : .portrait
}
var orientation: LayoutOrientation {
return self.size.width > self.size.height ? .landscape : .portrait
}
var standardKeyboardHeight: CGFloat {
return self.deviceMetrics.keyboardHeight(inLandscape: self.orientation == .landscape)
}
var standardInputHeight: CGFloat {
return self.deviceMetrics.standardInputHeight(inLandscape: self.orientation == .landscape)
}
static func concentricInsets(bottomInset: CGFloat, innerDiameter: CGFloat, sideInset: CGFloat) -> UIEdgeInsets {
let mappedBottomInset: CGFloat = max(bottomInset, sideInset)
return UIEdgeInsets(top: 0.0, left: sideInset, bottom: mappedBottomInset, right: sideInset)
}
}
@@ -0,0 +1,29 @@
import Foundation
import UIKit
import AsyncDisplayKit
public final class ContextContentContainerNode: ASDisplayNode {
public var contentNode: ContextContentNode?
override public init() {
super.init()
}
public func updateLayout(size: CGSize, scaledSize: CGSize, transition: ContainedViewLayoutTransition) {
guard let contentNode = self.contentNode else {
return
}
switch contentNode {
case .reference:
break
case .extracted:
break
case let .controller(controller):
transition.updatePosition(node: controller, position: CGPoint(x: scaledSize.width / 2.0, y: scaledSize.height / 2.0))
transition.updateBounds(node: controller, bounds: CGRect(origin: CGPoint(), size: size))
transition.updateTransformScale(node: controller, scale: scaledSize.width / size.width)
controller.updateLayout(size: size, transition: transition)
controller.controller.containerLayoutUpdated(ContainerViewLayout(size: size, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: .iPhoneX, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition)
}
}
}
@@ -0,0 +1,156 @@
import Foundation
import UIKit
import AsyncDisplayKit
open class ContextReferenceContentNode: ASDisplayNode {
override public init() {
super.init()
}
}
public final class ContextExtractedContentContainingNode: ASDisplayNode {
public let contentNode: ContextExtractedContentNode
public var contentRect: CGRect = CGRect()
public var isExtractedToContextPreview: Bool = false
public var willUpdateIsExtractedToContextPreview: ((Bool, ContainedViewLayoutTransition) -> Void)?
public var isExtractedToContextPreviewUpdated: ((Bool) -> Void)?
public var updateAbsoluteRect: ((CGRect, CGSize) -> Void)?
public var applyAbsoluteOffset: ((CGPoint, ContainedViewLayoutTransitionCurve, Double) -> Void)?
public var applyAbsoluteOffsetSpring: ((CGFloat, Double, CGFloat) -> Void)?
public var layoutUpdated: ((CGSize, ListViewItemUpdateAnimation) -> Void)?
public var updateDistractionFreeMode: ((Bool) -> Void)?
public var requestDismiss: (() -> Void)?
public var onDismiss: (() -> Void)?
public override init() {
self.contentNode = ContextExtractedContentNode()
super.init()
self.addSubnode(self.contentNode)
}
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.contentNode.supernode === self {
return self.contentNode.hitTest(self.view.convert(point, to: self.contentNode.view), with: event)
} else {
return nil
}
}
}
public final class ContextExtractedContentContainingView: UIView {
public let contentView: ContextExtractedContentView
public var contentRect: CGRect = CGRect()
public var isExtractedToContextPreview: Bool = false
public var willUpdateIsExtractedToContextPreview: ((Bool, ContainedViewLayoutTransition) -> Void)?
public var isExtractedToContextPreviewUpdated: ((Bool) -> Void)?
public var updateAbsoluteRect: ((CGRect, CGSize) -> Void)?
public var applyAbsoluteOffset: ((CGPoint, ContainedViewLayoutTransitionCurve, Double) -> Void)?
public var applyAbsoluteOffsetSpring: ((CGFloat, Double, CGFloat) -> Void)?
public var layoutUpdated: ((CGSize, ListViewItemUpdateAnimation) -> Void)?
public var updateDistractionFreeMode: ((Bool) -> Void)?
public var requestDismiss: (() -> Void)?
public var onDismiss: (() -> Void)?
public override init(frame: CGRect) {
self.contentView = ContextExtractedContentView()
super.init(frame: frame)
self.addSubview(self.contentView)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.contentView.superview === self {
return self.contentView.hitTest(self.convert(point, to: self.contentView), with: event)
} else {
return nil
}
}
}
public final class ContextExtractedContentNode: ASDisplayNode {
private var viewImpl: ContextExtractedContentView {
return self.view as! ContextExtractedContentView
}
public var customHitTest: ((CGPoint) -> UIView?)? {
didSet {
if self.isNodeLoaded {
self.viewImpl.customHitTest = self.customHitTest
}
}
}
override public init() {
super.init()
self.setViewBlock {
return ContextExtractedContentView(frame: CGRect())
}
}
}
public final class ContextExtractedContentView: UIView {
public var customHitTest: ((CGPoint) -> UIView?)?
override public init(frame: CGRect) {
super.init(frame: frame)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if result === self {
return nil
} else {
return result
}
}
}
public final class ContextControllerContentNode: ASDisplayNode {
public let sourceView: UIView
public let controller: ViewController
private let tapped: () -> Void
public init(sourceView: UIView, controller: ViewController, tapped: @escaping () -> Void) {
self.sourceView = sourceView
self.controller = controller
self.tapped = tapped
super.init()
self.addSubnode(controller.displayNode)
}
override public func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.tapped()
}
}
public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
transition.updateFrame(node: self.controller.displayNode, frame: CGRect(origin: CGPoint(), size: size))
}
}
public enum ContextContentNode {
case reference(view: UIView)
case extracted(node: ContextExtractedContentContainingNode, keepInPlace: Bool)
case controller(ContextControllerContentNode)
}
@@ -0,0 +1,321 @@
import Foundation
import UIKit
import AsyncDisplayKit
open class ContextControllerSourceNode: ContextReferenceContentNode {
public enum ShouldBegin {
case none
case `default`
case customActivationProcess
}
public private(set) var contextGesture: ContextGesture?
public var isGestureEnabled: Bool = true {
didSet {
self.contextGesture?.isEnabled = self.isGestureEnabled
}
}
public var beginDelay: Double = 0.12 {
didSet {
self.contextGesture?.beginDelay = self.beginDelay
}
}
public var animateScale: Bool = true
public var activated: ((ContextGesture, CGPoint) -> Void)?
public var shouldBegin: ((CGPoint) -> Bool)?
public var shouldBeginWithCustomActivationProcess: ((CGPoint) -> ShouldBegin)?
public var customActivationProgress: ((CGFloat, ContextGestureTransition) -> Void)?
public weak var additionalActivationProgressLayer: CALayer?
public var targetNodeForActivationProgress: ASDisplayNode?
public var targetNodeForActivationProgressContentRect: CGRect?
private var ignoreCurrentActivationProcess: Bool = false
public func cancelGesture() {
self.contextGesture?.cancel()
self.contextGesture?.isEnabled = false
self.contextGesture?.isEnabled = self.isGestureEnabled
self.ignoreCurrentActivationProcess = false
}
override open func didLoad() {
super.didLoad()
let contextGesture = ContextGesture(target: self, action: nil)
self.contextGesture = contextGesture
self.view.addGestureRecognizer(contextGesture)
contextGesture.beginDelay = self.beginDelay
contextGesture.isEnabled = self.isGestureEnabled
contextGesture.shouldBegin = { [weak self] point in
guard let self, !self.bounds.width.isZero else {
return false
}
if let shouldBeginWithCustomActivationProcess = self.shouldBeginWithCustomActivationProcess {
let result = shouldBeginWithCustomActivationProcess(point)
switch result {
case .none:
self.ignoreCurrentActivationProcess = false
return false
case .default:
self.ignoreCurrentActivationProcess = false
return true
case .customActivationProcess:
self.ignoreCurrentActivationProcess = true
return true
}
} else {
self.ignoreCurrentActivationProcess = false
return self.shouldBegin?(point) ?? true
}
}
contextGesture.activationProgress = { [weak self] progress, update in
guard let strongSelf = self, !strongSelf.bounds.width.isZero else {
return
}
if let customActivationProgress = strongSelf.customActivationProgress {
customActivationProgress(progress, update)
} else if strongSelf.animateScale && !strongSelf.ignoreCurrentActivationProcess {
let targetNode: ASDisplayNode
let targetContentRect: CGRect
if let targetNodeForActivationProgress = strongSelf.targetNodeForActivationProgress {
targetNode = targetNodeForActivationProgress
if let targetNodeForActivationProgressContentRect = strongSelf.targetNodeForActivationProgressContentRect {
targetContentRect = targetNodeForActivationProgressContentRect
} else {
targetContentRect = CGRect(origin: CGPoint(), size: targetNode.bounds.size)
}
} else {
targetNode = strongSelf
targetContentRect = CGRect(origin: CGPoint(), size: targetNode.bounds.size)
}
let scaleSide = targetContentRect.width
let minScale: CGFloat = max(0.7, (scaleSide - 15.0) / scaleSide)
let currentScale = 1.0 * (1.0 - progress) + minScale * progress
let originalCenterOffsetX: CGFloat = targetNode.bounds.width / 2.0 - targetContentRect.midX
let scaledCenterOffsetX: CGFloat = originalCenterOffsetX * currentScale
let originalCenterOffsetY: CGFloat = targetNode.bounds.height / 2.0 - targetContentRect.midY
let scaledCenterOffsetY: CGFloat = originalCenterOffsetY * currentScale
let scaleMidX: CGFloat = scaledCenterOffsetX - originalCenterOffsetX
let scaleMidY: CGFloat = scaledCenterOffsetY - originalCenterOffsetY
switch update {
case .update:
let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0)
targetNode.layer.sublayerTransform = sublayerTransform
if let additionalActivationProgressLayer = strongSelf.additionalActivationProgressLayer {
additionalActivationProgressLayer.transform = sublayerTransform
}
case .begin:
let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0)
targetNode.layer.sublayerTransform = sublayerTransform
if let additionalActivationProgressLayer = strongSelf.additionalActivationProgressLayer {
additionalActivationProgressLayer.transform = sublayerTransform
}
case .ended:
let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0)
let previousTransform = targetNode.layer.sublayerTransform
targetNode.layer.sublayerTransform = sublayerTransform
targetNode.layer.animate(from: NSValue(caTransform3D: previousTransform), to: NSValue(caTransform3D: sublayerTransform), keyPath: "sublayerTransform", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2)
if let additionalActivationProgressLayer = strongSelf.additionalActivationProgressLayer {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2, execute: {
additionalActivationProgressLayer.transform = sublayerTransform
})
}
}
}
}
contextGesture.activated = { [weak self] gesture, location in
guard let strongSelf = self else {
gesture.cancel()
return
}
if let customActivationProgress = strongSelf.customActivationProgress {
customActivationProgress(0.0, .ended(0.0))
}
if let activated = strongSelf.activated {
activated(gesture, location)
} else {
gesture.cancel()
}
}
contextGesture.isEnabled = self.isGestureEnabled
}
}
open class ContextControllerSourceView: UIView {
public private(set) var contextGesture: ContextGesture?
public var isGestureEnabled: Bool = true {
didSet {
self.contextGesture?.isEnabled = self.isGestureEnabled
}
}
public var beginDelay: Double = 0.12 {
didSet {
self.contextGesture?.beginDelay = self.beginDelay
}
}
public var animateScale: Bool = true
public var activated: ((ContextGesture, CGPoint) -> Void)?
public var shouldBegin: ((CGPoint) -> Bool)?
public var customActivationProgress: ((CGFloat, ContextGestureTransition) -> Void)?
public weak var additionalActivationProgressLayer: CALayer?
public var targetNodeForActivationProgress: ASDisplayNode?
public var targetViewForActivationProgress: UIView?
public weak var targetLayerForActivationProgress: CALayer?
public var targetNodeForActivationProgressContentRect: CGRect?
public var useSublayerTransformForActivation: Bool = true
override public init(frame: CGRect) {
super.init(frame: frame)
self.isMultipleTouchEnabled = false
self.isExclusiveTouch = true
let contextGesture = ContextGesture(target: self, action: nil)
self.contextGesture = contextGesture
self.addGestureRecognizer(contextGesture)
contextGesture.beginDelay = self.beginDelay
contextGesture.isEnabled = self.isGestureEnabled
contextGesture.shouldBegin = { [weak self] point in
guard let strongSelf = self, !strongSelf.bounds.width.isZero else {
return false
}
return strongSelf.shouldBegin?(point) ?? true
}
contextGesture.activationProgress = { [weak self] progress, update in
guard let strongSelf = self, !strongSelf.bounds.width.isZero else {
return
}
if let customActivationProgress = strongSelf.customActivationProgress {
customActivationProgress(progress, update)
} else if strongSelf.animateScale {
let targetLayer: CALayer
let targetContentRect: CGRect
if let targetNodeForActivationProgress = strongSelf.targetNodeForActivationProgress {
targetLayer = targetNodeForActivationProgress.layer
if let targetNodeForActivationProgressContentRect = strongSelf.targetNodeForActivationProgressContentRect {
targetContentRect = targetNodeForActivationProgressContentRect
} else {
targetContentRect = CGRect(origin: CGPoint(), size: targetLayer.bounds.size)
}
} else if let targetViewForActivationProgress = strongSelf.targetViewForActivationProgress {
targetLayer = targetViewForActivationProgress.layer
if let targetNodeForActivationProgressContentRect = strongSelf.targetNodeForActivationProgressContentRect {
targetContentRect = targetNodeForActivationProgressContentRect
} else {
targetContentRect = CGRect(origin: CGPoint(), size: targetLayer.bounds.size)
}
} else if let targetLayerForActivationProgress = strongSelf.targetLayerForActivationProgress {
targetLayer = targetLayerForActivationProgress
if let targetNodeForActivationProgressContentRect = strongSelf.targetNodeForActivationProgressContentRect {
targetContentRect = targetNodeForActivationProgressContentRect
} else {
targetContentRect = CGRect(origin: CGPoint(), size: targetLayer.bounds.size)
}
} else {
targetLayer = strongSelf.layer
targetContentRect = CGRect(origin: CGPoint(), size: targetLayer.bounds.size)
}
let scaleSide = targetContentRect.width
let minScale: CGFloat = max(0.7, (scaleSide - 15.0) / scaleSide)
let currentScale = 1.0 * (1.0 - progress) + minScale * progress
let originalCenterOffsetX: CGFloat = targetLayer.bounds.width / 2.0 - targetContentRect.midX
let scaledCenterOffsetX: CGFloat = originalCenterOffsetX * currentScale
let originalCenterOffsetY: CGFloat = targetLayer.bounds.height / 2.0 - targetContentRect.midY
let scaledCenterOffsetY: CGFloat = originalCenterOffsetY * currentScale
let scaleMidX: CGFloat = scaledCenterOffsetX - originalCenterOffsetX
let scaleMidY: CGFloat = scaledCenterOffsetY - originalCenterOffsetY
switch update {
case .update:
let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0)
if strongSelf.useSublayerTransformForActivation {
targetLayer.sublayerTransform = sublayerTransform
} else {
targetLayer.transform = sublayerTransform
}
if let additionalActivationProgressLayer = strongSelf.additionalActivationProgressLayer {
additionalActivationProgressLayer.transform = sublayerTransform
}
case .begin:
let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0)
if strongSelf.useSublayerTransformForActivation {
targetLayer.sublayerTransform = sublayerTransform
} else {
targetLayer.transform = sublayerTransform
}
if let additionalActivationProgressLayer = strongSelf.additionalActivationProgressLayer {
additionalActivationProgressLayer.transform = sublayerTransform
}
case .ended:
let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0)
if strongSelf.useSublayerTransformForActivation {
let previousTransform = targetLayer.sublayerTransform
targetLayer.sublayerTransform = sublayerTransform
targetLayer.animate(from: NSValue(caTransform3D: previousTransform), to: NSValue(caTransform3D: sublayerTransform), keyPath: "sublayerTransform", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2)
} else {
let previousTransform = targetLayer.transform
targetLayer.transform = sublayerTransform
targetLayer.animate(from: NSValue(caTransform3D: previousTransform), to: NSValue(caTransform3D: sublayerTransform), keyPath: "transform", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2)
}
if let additionalActivationProgressLayer = strongSelf.additionalActivationProgressLayer {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2, execute: {
additionalActivationProgressLayer.transform = sublayerTransform
})
}
}
}
}
contextGesture.activated = { [weak self] gesture, location in
guard let strongSelf = self else {
gesture.cancel()
return
}
if let customActivationProgress = strongSelf.customActivationProgress {
customActivationProgress(0.0, .ended(0.0))
}
if let activated = strongSelf.activated {
activated(gesture, location)
} else {
gesture.cancel()
}
}
contextGesture.isEnabled = self.isGestureEnabled
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func cancelGesture() {
self.contextGesture?.cancel()
self.contextGesture?.isEnabled = false
self.contextGesture?.isEnabled = self.isGestureEnabled
}
}
@@ -0,0 +1,280 @@
import Foundation
import UIKit
import AsyncDisplayKit
public enum ContextGestureTransition {
case begin
case update
case ended(CGFloat)
}
private class TimerTargetWrapper: NSObject {
let f: () -> Void
init(_ f: @escaping () -> Void) {
self.f = f
}
@objc func timerEvent() {
self.f()
}
}
public func cancelParentGestures(view: UIView, ignore: [UIGestureRecognizer] = []) {
if let gestureRecognizers = view.gestureRecognizers {
for recognizer in gestureRecognizers {
if ignore.contains(where: { $0 === recognizer }) {
continue
}
recognizer.state = .failed
}
}
if let node = (view as? ListViewBackingView)?.target {
node.cancelSelection()
}
if let node = view.asyncdisplaykit_node as? HighlightTrackingButtonNode {
node.highligthedChanged(false)
}
if let superview = view.superview {
cancelParentGestures(view: superview, ignore: ignore)
}
}
private func cancelOtherGestures(gesture: ContextGesture, view: UIView) {
if let gestureRecognizers = view.gestureRecognizers {
for recognizer in gestureRecognizers {
if let recognizer = recognizer as? ContextGesture, recognizer !== gesture {
recognizer.cancel()
} else if let recognizer = recognizer as? ListViewTapGestureRecognizer {
recognizer.cancel()
} else if let recognizer = recognizer as? UITapGestureRecognizer {
switch recognizer.state {
case .possible:
recognizer.state = .failed
default:
break
}
}
}
}
for subview in view.subviews {
cancelOtherGestures(gesture: gesture, view: subview)
}
}
private final class InternalGestureRecognizerDelegate: NSObject, UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if otherGestureRecognizer is UIPanGestureRecognizer {
return false
}
return true
}
}
public final class ContextGesture: UIGestureRecognizer, UIGestureRecognizerDelegate {
private let internalDelegate = InternalGestureRecognizerDelegate()
public var beginDelay: Double = 0.12
public var activateOnTap: Bool = false
private var currentProgress: CGFloat = 0.0
private var delayTimer: Timer?
private var animator: DisplayLinkAnimator?
private var isValidated: Bool = false
private var wasActivated: Bool = false
public var shouldBegin: ((CGPoint) -> Bool)?
public var activationProgress: ((CGFloat, ContextGestureTransition) -> Void)?
public var activated: ((ContextGesture, CGPoint) -> Void)?
public var externalUpdated: ((UIView?, CGPoint) -> Void)?
public var externalEnded: (((UIView?, CGPoint)?) -> Void)?
public var activatedAfterCompletion: ((CGPoint, Bool) -> Void)?
public var cancelGesturesOnActivation: (() -> Void)?
override public init(target: Any?, action: Selector?) {
super.init(target: target, action: action)
self.delegate = self.internalDelegate
}
override public func reset() {
super.reset()
self.endPressedAppearance()
self.currentProgress = 0.0
self.delayTimer?.invalidate()
self.delayTimer = nil
self.isValidated = false
self.externalUpdated = nil
self.externalEnded = nil
self.animator?.invalidate()
self.animator = nil
self.wasActivated = false
}
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
guard let touch = touches.first else {
return
}
let location = touch.location(in: self.view)
if let shouldBegin = self.shouldBegin {
if !shouldBegin(location) {
self.state = .failed
return
}
}
let windowLocation = touch.location(in: nil)
if windowLocation.x < 8.0 {
self.state = .failed
return
}
if self.delayTimer == nil {
let delayTimer = Timer(timeInterval: self.beginDelay, target: TimerTargetWrapper { [weak self] in
guard let strongSelf = self, let _ = strongSelf.delayTimer else {
return
}
strongSelf.isValidated = true
if strongSelf.animator == nil {
strongSelf.animator = DisplayLinkAnimator(duration: 0.2, from: 0.0, to: 1.0, update: { value in
guard let strongSelf = self else {
return
}
if strongSelf.isValidated {
strongSelf.currentProgress = value
strongSelf.activationProgress?(value, .update)
}
}, completion: {
guard let strongSelf = self else {
return
}
switch strongSelf.state {
case .possible:
strongSelf.delayTimer?.invalidate()
strongSelf.animator?.invalidate()
strongSelf.activated?(strongSelf, location)
strongSelf.wasActivated = true
if let view = strongSelf.view {
if let window = view.window {
cancelOtherGestures(gesture: strongSelf, view: window)
}
strongSelf.cancelGesturesOnActivation?()
cancelParentGestures(view: view, ignore: [strongSelf])
}
strongSelf.state = .began
default:
break
}
})
}
strongSelf.activationProgress?(strongSelf.currentProgress, .begin)
}, selector: #selector(TimerTargetWrapper.timerEvent), userInfo: nil, repeats: false)
self.delayTimer = delayTimer
RunLoop.main.add(delayTimer, forMode: .common)
}
}
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
if let touch = touches.first {
if #available(iOS 9.0, *) {
let maxForce: CGFloat = max(2.5, min(3.0, touch.maximumPossibleForce))
if touch.force >= maxForce {
if !self.isValidated {
self.isValidated = true
}
switch self.state {
case .possible:
self.delayTimer?.invalidate()
self.animator?.invalidate()
self.activated?(self, touch.location(in: self.view))
self.wasActivated = true
if let view = self.view?.superview {
if let window = view.window {
cancelOtherGestures(gesture: self, view: window)
}
cancelParentGestures(view: view)
}
self.state = .began
default:
break
}
}
}
self.externalUpdated?(self.view, touch.location(in: self.view))
}
}
override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
if let touch = touches.first {
if !self.currentProgress.isZero, self.isValidated {
self.currentProgress = 0.0
self.activationProgress?(0.0, .ended(self.currentProgress))
if self.wasActivated {
self.activatedAfterCompletion?(touch.location(in: self.view), false)
}
} else {
self.currentProgress = 0.0
if !self.wasActivated && self.activateOnTap {
self.activatedAfterCompletion?(touch.location(in: self.view), true)
}
}
self.externalEnded?((self.view, touch.location(in: self.view)))
}
self.delayTimer?.invalidate()
self.animator?.invalidate()
self.state = .failed
}
override public func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
if let _ = touches.first, !self.currentProgress.isZero, self.isValidated {
let previousProgress = self.currentProgress
self.currentProgress = 0.0
self.activationProgress?(0.0, .ended(previousProgress))
}
self.delayTimer?.invalidate()
self.animator?.invalidate()
self.state = .failed
}
public func cancel() {
if !self.currentProgress.isZero, self.isValidated {
let previousProgress = self.currentProgress
self.currentProgress = 0.0
self.activationProgress?(0.0, .ended(previousProgress))
self.delayTimer?.invalidate()
self.animator?.invalidate()
self.state = .failed
} else {
self.state = .failed
}
}
public func endPressedAppearance() {
if !self.currentProgress.isZero, self.isValidated {
let previousProgress = self.currentProgress
self.currentProgress = 0.0
self.delayTimer?.invalidate()
self.animator?.invalidate()
self.isValidated = false
self.activationProgress?(0.0, .ended(previousProgress))
}
}
}
@@ -0,0 +1,18 @@
import UIKit
public enum ContextMenuActionContent {
case text(title: String, accessibilityLabel: String)
case icon(UIImage)
case textWithIcon(title: String, icon: UIImage?)
case textWithSubtitleAndIcon(title: String, subtitle: String, icon: UIImage?)
}
public struct ContextMenuAction {
public let content: ContextMenuActionContent
public let action: () -> Void
public init(content: ContextMenuActionContent, action: @escaping () -> Void) {
self.content = content
self.action = action
}
}
@@ -0,0 +1,111 @@
import Foundation
import UIKit
import AsyncDisplayKit
private struct CachedMaskParams: Equatable {
let size: CGSize
let relativeArrowPosition: CGFloat
let arrowOnBottom: Bool
}
private final class ContextMenuContainerMaskView: UIView {
override class var layerClass: AnyClass {
return CAShapeLayer.self
}
}
public final class ContextMenuContainerNode: ASDisplayNode {
private var cachedMaskParams: CachedMaskParams?
private let maskView = ContextMenuContainerMaskView()
public let containerNode: ASDisplayNode
public var relativeArrowPosition: (CGFloat, Bool)?
private var effectView: UIVisualEffectView?
public init(isBlurred: Bool, isDark: Bool) {
self.containerNode = ASDisplayNode()
super.init()
if isBlurred {
let effectView = UIVisualEffectView(effect: UIBlurEffect(style: isDark ? .dark : .light))
self.containerNode.view.addSubview(effectView)
self.effectView = effectView
} else {
self.containerNode.backgroundColor = isDark ? UIColor(rgb: 0x2f2f2f) : UIColor(rgb: 0xF8F8F6)
}
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowRadius = 10.0
self.layer.shadowOpacity = 0.2
self.layer.shadowOffset = CGSize(width: 0.0, height: 5.0)
self.containerNode.view.mask = self.maskView
self.addSubnode(self.containerNode)
}
override public func didLoad() {
super.didLoad()
self.layer.allowsGroupOpacity = true
}
override public func layout() {
super.layout()
self.updateLayout(transition: .immediate)
}
public func updateLayout(transition: ContainedViewLayoutTransition) {
transition.updateFrame(node: self.containerNode, frame: self.bounds)
self.effectView?.frame = self.bounds
let maskParams = CachedMaskParams(size: self.bounds.size, relativeArrowPosition: self.relativeArrowPosition?.0 ?? self.bounds.size.width / 2.0, arrowOnBottom: self.relativeArrowPosition?.1 ?? true)
if self.cachedMaskParams != maskParams {
let path = UIBezierPath()
let cornerRadius: CGFloat = 10.0
let verticalInset: CGFloat = 9.0
let arrowWidth: CGFloat = 18.0
let requestedArrowPosition = maskParams.relativeArrowPosition
let arrowPosition = max(cornerRadius + arrowWidth / 2.0, min(maskParams.size.width - cornerRadius - arrowWidth / 2.0, requestedArrowPosition))
let arrowOnBottom = maskParams.arrowOnBottom
path.move(to: CGPoint(x: 0.0, y: verticalInset + cornerRadius))
path.addArc(withCenter: CGPoint(x: cornerRadius, y: verticalInset + cornerRadius), radius: cornerRadius, startAngle: CGFloat.pi, endAngle: CGFloat(3.0 * CGFloat.pi / 2.0), clockwise: true)
if !arrowOnBottom {
path.addLine(to: CGPoint(x: arrowPosition - arrowWidth / 2.0, y: verticalInset))
path.addLine(to: CGPoint(x: arrowPosition, y: 0.0))
path.addLine(to: CGPoint(x: arrowPosition + arrowWidth / 2.0, y: verticalInset))
}
path.addLine(to: CGPoint(x: maskParams.size.width - cornerRadius, y: verticalInset))
path.addArc(withCenter: CGPoint(x: maskParams.size.width - cornerRadius, y: verticalInset + cornerRadius), radius: cornerRadius, startAngle: CGFloat(3.0 * CGFloat.pi / 2.0), endAngle: 0.0, clockwise: true)
path.addLine(to: CGPoint(x: maskParams.size.width, y: maskParams.size.height - cornerRadius - verticalInset))
path.addArc(withCenter: CGPoint(x: maskParams.size.width - cornerRadius, y: maskParams.size.height - cornerRadius - verticalInset), radius: cornerRadius, startAngle: 0.0, endAngle: CGFloat(CGFloat.pi / 2.0), clockwise: true)
if arrowOnBottom {
path.addLine(to: CGPoint(x: arrowPosition + arrowWidth / 2.0, y: maskParams.size.height - verticalInset))
path.addLine(to: CGPoint(x: arrowPosition, y: maskParams.size.height))
path.addLine(to: CGPoint(x: arrowPosition - arrowWidth / 2.0, y: maskParams.size.height - verticalInset))
}
path.addLine(to: CGPoint(x: cornerRadius, y: maskParams.size.height - verticalInset))
path.addArc(withCenter: CGPoint(x: cornerRadius, y: maskParams.size.height - cornerRadius - verticalInset), radius: cornerRadius, startAngle: CGFloat(CGFloat.pi / 2.0), endAngle: CGFloat.pi, clockwise: true)
path.close()
self.cachedMaskParams = maskParams
if let layer = self.maskView.layer as? CAShapeLayer {
if case let .animated(duration, curve) = transition, let previousPath = layer.path {
layer.animate(from: previousPath, to: path.cgPath, keyPath: "path", timingFunction: curve.timingFunction, duration: duration)
}
layer.path = path.cgPath
}
if case let .animated(duration, curve) = transition, let previousPath = self.layer.shadowPath {
self.layer.shadowPath = path.cgPath
self.layer.animate(from: previousPath, to: path.cgPath, keyPath: "shadowPath", timingFunction: curve.timingFunction, duration: duration)
} else {
self.layer.shadowPath = path.cgPath
}
}
}
}
@@ -0,0 +1,57 @@
import Foundation
import UIKit
import AsyncDisplayKit
public final class ContextMenuControllerPresentationArguments {
public let sourceNodeAndRect: () -> (ASDisplayNode, CGRect, ASDisplayNode, CGRect)?
public let bounce: Bool
public init(sourceNodeAndRect: @escaping () -> (ASDisplayNode, CGRect, ASDisplayNode, CGRect)?, bounce: Bool = true) {
self.sourceNodeAndRect = sourceNodeAndRect
self.bounce = bounce
}
}
public protocol ContextMenuController: ViewController, StandalonePresentableController {
var centerHorizontally: Bool { get set }
var dismissed: (() -> Void)? { get set }
var dismissOnTap: ((UIView, CGPoint) -> Bool)? { get set }
}
public struct ContextMenuControllerArguments {
public var actions: [ContextMenuAction]
public var catchTapsOutside: Bool
public var hasHapticFeedback: Bool
public var blurred: Bool
public var skipCoordnateConversion: Bool
public var isDark: Bool
public init(actions: [ContextMenuAction], catchTapsOutside: Bool, hasHapticFeedback: Bool, blurred: Bool, skipCoordnateConversion: Bool, isDark: Bool) {
self.actions = actions
self.catchTapsOutside = catchTapsOutside
self.hasHapticFeedback = hasHapticFeedback
self.blurred = blurred
self.skipCoordnateConversion = skipCoordnateConversion
self.isDark = isDark
}
}
private var contextMenuControllerProvider: ((ContextMenuControllerArguments) -> ContextMenuController)?
public func setContextMenuControllerProvider(_ f: @escaping (ContextMenuControllerArguments) -> ContextMenuController) {
contextMenuControllerProvider = f
}
public func makeContextMenuController(actions: [ContextMenuAction], catchTapsOutside: Bool = false, hasHapticFeedback: Bool = false, blurred: Bool = false, isDark: Bool = true, skipCoordnateConversion: Bool = false) -> ContextMenuController {
guard let contextMenuControllerProvider = contextMenuControllerProvider else {
preconditionFailure()
}
return contextMenuControllerProvider(ContextMenuControllerArguments(
actions: actions,
catchTapsOutside: catchTapsOutside,
hasHapticFeedback: hasHapticFeedback,
blurred: blurred,
skipCoordnateConversion: skipCoordnateConversion,
isDark: isDark
))
}
@@ -0,0 +1,418 @@
import UIKit
public enum DeviceType {
case phone
case tablet
}
public enum DeviceMetrics: CaseIterable, Equatable {
public struct Performance {
public let isGraphicallyCapable: Bool
init() {
var length: Int = 4
var cpuCount: UInt32 = 0
sysctlbyname("hw.ncpu", &cpuCount, &length, nil, 0)
self.isGraphicallyCapable = cpuCount >= 4
}
}
case iPhone4
case iPhone5
case iPhone6
case iPhone6Plus
case iPhoneX
case iPhoneXSMax
case iPhoneXr
case iPhone12Mini
case iPhone12
case iPhone12ProMax
case iPhone13Mini
case iPhone13
case iPhone13Pro
case iPhone13ProMax
case iPhone14Pro
case iPhone14ProZoomed
case iPhone14ProMax
case iPhone14ProMaxZoomed
case iPhone16Pro
case iPhone16ProMax
case iPhoneAir
case iPad
case iPadMini
case iPad102Inch
case iPadPro10Inch
case iPadPro11Inch
case iPadPro
case iPadPro3rdGen
case iPadMini6thGen
case unknown(screenSize: CGSize, statusBarHeight: CGFloat, onScreenNavigationHeight: CGFloat?, screenCornerRadius: CGFloat)
public static let performance = Performance()
public static var allCases: [DeviceMetrics] {
return [
.iPhone4,
.iPhone5,
.iPhone6,
.iPhone6Plus,
.iPhoneX,
.iPhoneXSMax,
.iPhoneXr,
.iPhone12Mini,
.iPhone12,
.iPhone12ProMax,
.iPhone13Mini,
.iPhone13,
.iPhone13Pro,
.iPhone13ProMax,
.iPhone14Pro,
.iPhone14ProZoomed,
.iPhone14ProMax,
.iPhone14ProMaxZoomed,
.iPhone16Pro,
.iPhone16ProMax,
.iPhoneAir,
.iPad,
.iPadMini,
.iPad102Inch,
.iPadPro10Inch,
.iPadPro11Inch,
.iPadPro,
.iPadPro3rdGen,
.iPadMini6thGen
]
}
public init(screenSize: CGSize, scale: CGFloat, statusBarHeight: CGFloat, onScreenNavigationHeight: CGFloat?) {
var screenSize = screenSize
if screenSize.width > screenSize.height {
screenSize = CGSize(width: screenSize.height, height: screenSize.width)
}
let additionalSize = CGSize(width: screenSize.width, height: screenSize.height + 20.0)
for device in DeviceMetrics.allCases {
if let _ = onScreenNavigationHeight, device.onScreenNavigationHeight(inLandscape: false, systemOnScreenNavigationHeight: nil) == nil {
if case .tablet = device.type {
if screenSize.height == 1024.0 && screenSize.width == 768.0 {
} else {
continue
}
} else {
continue
}
}
let width = device.screenSize.width
let height = device.screenSize.height
if ((screenSize.width.isEqual(to: width) && screenSize.height.isEqual(to: height)) || (additionalSize.width.isEqual(to: width) && additionalSize.height.isEqual(to: height))) {
if case .iPhoneX = device, statusBarHeight == 47.0 {
self = .iPhone14ProMaxZoomed
} else if case .iPhoneXSMax = device, scale == 2.0 {
self = .iPhoneXr
} else {
self = device
}
return
}
}
let screenCornerRadius: CGFloat
if screenSize.width >= 1024.0 || screenSize.height >= 1024.0 {
screenCornerRadius = 0.0
} else if onScreenNavigationHeight != nil {
screenCornerRadius = 39.0
} else {
screenCornerRadius = 0.0
}
self = .unknown(screenSize: screenSize, statusBarHeight: statusBarHeight, onScreenNavigationHeight: onScreenNavigationHeight, screenCornerRadius: screenCornerRadius)
}
public var type: DeviceType {
switch self {
case .iPad, .iPad102Inch, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen:
return .tablet
case let .unknown(screenSize, _, _, _) where screenSize.width >= 744.0 && screenSize.height >= 1024.0:
return .tablet
default:
return .phone
}
}
public var screenSize: CGSize {
switch self {
case .iPhone4:
return CGSize(width: 320.0, height: 480.0)
case .iPhone5:
return CGSize(width: 320.0, height: 568.0)
case .iPhone6:
return CGSize(width: 375.0, height: 667.0)
case .iPhone6Plus:
return CGSize(width: 414.0, height: 736.0)
case .iPhoneX:
return CGSize(width: 375.0, height: 812.0)
case .iPhoneXSMax, .iPhoneXr:
return CGSize(width: 414.0, height: 896.0)
case .iPhone12Mini:
return CGSize(width: 360.0, height: 780.0)
case .iPhone12:
return CGSize(width: 390.0, height: 844.0)
case .iPhone12ProMax:
return CGSize(width: 428.0, height: 926.0)
case .iPhone13Mini:
return CGSize(width: 375.0, height: 812.0)
case .iPhone13:
return CGSize(width: 390.0, height: 844.0)
case .iPhone13Pro:
return CGSize(width: 390.0, height: 844.0)
case .iPhone13ProMax:
return CGSize(width: 428.0, height: 926.0)
case .iPhone14Pro:
return CGSize(width: 393.0, height: 852.0)
case .iPhone14ProZoomed:
return CGSize(width: 320.0, height: 693.0)
case .iPhone14ProMax:
return CGSize(width: 430.0, height: 932.0)
case .iPhone14ProMaxZoomed:
return CGSize(width: 375.0, height: 812.0)
case .iPhone16Pro:
return CGSize(width: 402.0, height: 874.0)
case .iPhone16ProMax:
return CGSize(width: 440.0, height: 956.0)
case .iPhoneAir:
return CGSize(width: 420.0, height: 912.0)
case .iPad:
return CGSize(width: 768.0, height: 1024.0)
case .iPadMini:
return CGSize(width: 744.0, height: 1133.0)
case .iPad102Inch:
return CGSize(width: 810.0, height: 1080.0)
case .iPadPro10Inch:
return CGSize(width: 834.0, height: 1112.0)
case .iPadPro11Inch:
return CGSize(width: 834.0, height: 1194.0)
case .iPadPro, .iPadPro3rdGen:
return CGSize(width: 1024.0, height: 1366.0)
case .iPadMini6thGen:
return CGSize(width: 744.0, height: 1133.0)
case let .unknown(screenSize, _, _, _):
return screenSize
}
}
public var screenCornerRadius: CGFloat {
switch self {
case .iPhoneX, .iPhoneXSMax:
return 39.0
case .iPhoneXr:
return 41.5
case .iPhone12Mini:
return 44.0
case .iPhone12, .iPhone13, .iPhone13Pro, .iPhone14ProZoomed:
return 47.0 + UIScreenPixel
case .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMaxZoomed:
return 53.0 + UIScreenPixel
case .iPhone14Pro, .iPhone14ProMax:
return 55.0
case .iPhone16Pro, .iPhone16ProMax:
return 62.0
case .iPhoneAir:
return 62.0
case let .unknown(_, _, _, screenCornerRadius):
return screenCornerRadius
default:
return 0.0
}
}
func safeInsets(inLandscape: Bool) -> UIEdgeInsets {
switch self {
case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone14ProZoomed, .iPhone14ProMaxZoomed:
return inLandscape ? UIEdgeInsets(top: 0.0, left: 44.0, bottom: 0.0, right: 44.0) : UIEdgeInsets(top: 44.0, left: 0.0, bottom: 0.0, right: 0.0)
case .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax:
return inLandscape ? UIEdgeInsets(top: 0.0, left: 47.0, bottom: 0.0, right: 47.0) : UIEdgeInsets(top: 44.0, left: 0.0, bottom: 0.0, right: 0.0)
case .iPhone14Pro, .iPhone14ProMax, .iPhone16Pro, .iPhone16ProMax:
return inLandscape ? UIEdgeInsets(top: 0.0, left: 59.0, bottom: 0.0, right: 59.0) : UIEdgeInsets(top: 44.0, left: 0.0, bottom: 0.0, right: 0.0)
case .iPhoneAir:
return inLandscape ? UIEdgeInsets(top: 0.0, left: 68.0, bottom: 0.0, right: 68.0) : UIEdgeInsets(top: 68.0, left: 0.0, bottom: 0.0, right: 0.0)
default:
return UIEdgeInsets.zero
}
}
public func onScreenNavigationHeight(inLandscape: Bool, systemOnScreenNavigationHeight: CGFloat?) -> CGFloat? {
switch self {
case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProMax, .iPhone16Pro, .iPhone16ProMax, .iPhoneAir:
if #available(iOS 26.0, *) {
return 20.0
} else {
return inLandscape ? 21.0 : 34.0
}
case .iPhone14ProZoomed:
return inLandscape ? 21.0 : 28.0
case .iPhone14ProMaxZoomed:
return inLandscape ? 21.0 : 31.0
case .iPadPro3rdGen, .iPadPro11Inch:
return 21.0
case .iPad, .iPadPro, .iPadPro10Inch, .iPadMini, .iPadMini6thGen:
if let systemOnScreenNavigationHeight = systemOnScreenNavigationHeight, !systemOnScreenNavigationHeight.isZero {
return 21.0
} else {
return nil
}
case let .unknown(_, _, onScreenNavigationHeight, _):
return onScreenNavigationHeight
default:
return nil
}
}
func statusBarHeight(for size: CGSize) -> CGFloat? {
let value = self.statusBarHeight
if self.type == .tablet {
return value
} else {
if size.width < size.height {
return value
} else {
return nil
}
}
}
var statusBarHeight: CGFloat {
switch self {
case .iPhone14Pro, .iPhone14ProMax:
return 54.0
case .iPhone14ProMaxZoomed:
return 47.0
case .iPhone16Pro, .iPhone16ProMax:
return 54.0
case .iPhoneAir:
return 59.0
case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax:
return 44.0
case .iPadPro11Inch, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen:
return 24.0
case let .unknown(_, statusBarHeight, _, _):
return statusBarHeight
default:
return 20.0
}
}
public func keyboardHeight(inLandscape: Bool) -> CGFloat {
var keyboardHeight = _keyboardHeight(inLandscape: inLandscape)
if #available(iOS 26.0, *) {
if !inLandscape {
keyboardHeight += 9.0
}
}
return keyboardHeight
}
private func _keyboardHeight(inLandscape: Bool) -> CGFloat {
if inLandscape {
switch self {
case .iPhone4, .iPhone5:
return 162.0
case .iPhone6, .iPhone6Plus:
return 163.0
case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax:
return 172.0
case .iPhoneAir:
return 172.0
case .iPad, .iPad102Inch, .iPadPro10Inch:
return 348.0
case .iPadPro11Inch, .iPadMini, .iPadMini6thGen:
return 368.0
case .iPadPro:
return 421.0
case .iPadPro3rdGen:
return 441.0
case .unknown:
return 216.0
}
} else {
switch self {
case .iPhone4, .iPhone5, .iPhone6:
return 216.0
case .iPhone6Plus:
return 226.0
case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed, .iPhone16Pro:
return 292.0
case .iPhoneAir:
return 292.0
case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone16ProMax:
return 302.0
case .iPad, .iPad102Inch, .iPadPro10Inch:
return 263.0
case .iPadPro11Inch:
return 283.0
case .iPadPro, .iPadMini, .iPadMini6thGen:
return 328.0
case .iPadPro3rdGen:
return 348.0
case .unknown:
return 216.0
}
}
}
func predictiveInputHeight(inLandscape: Bool) -> CGFloat {
if inLandscape {
switch self {
case .iPhone4, .iPhone5, .iPhone6, .iPhone6Plus, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax, .iPhoneAir:
return 37.0
case .iPad, .iPad102Inch, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen:
return 50.0
case .unknown:
return 37.0
}
} else {
switch self {
case .iPhone4, .iPhone5:
return 37.0
case .iPhone6, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax, .iPhoneAir:
return 44.0
case .iPhone6Plus:
return 45.0
case .iPad, .iPad102Inch, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen:
return 50.0
case .unknown:
return 44.0
}
}
}
public func standardInputHeight(inLandscape: Bool) -> CGFloat {
return self.keyboardHeight(inLandscape: inLandscape) + predictiveInputHeight(inLandscape: inLandscape)
}
public var hasTopNotch: Bool {
switch self {
case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax:
return true
default:
return false
}
}
public var hasDynamicIsland: Bool {
switch self {
case .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax, .iPhoneAir:
return true
default:
return false
}
}
public var showAppBadge: Bool {
if case .iPhoneX = self {
return false
}
return self.hasTopNotch
}
}
@@ -0,0 +1,387 @@
import Foundation
import UIKit
import Darwin
public protocol SharedDisplayLinkDriverLink: AnyObject {
var isPaused: Bool { get set }
func invalidate()
}
private let isIpad: Bool = {
var systemInfo = utsname()
uname(&systemInfo)
let modelCode = withUnsafePointer(to: &systemInfo.machine) {
$0.withMemoryRebound(to: CChar.self, capacity: 1) {
ptr in String.init(validatingUTF8: ptr)
}
}
if let modelCode {
if modelCode.lowercased().hasPrefix("ipad") {
return true
}
}
return false
}()
public final class SharedDisplayLinkDriver {
public enum FramesPerSecond: Comparable {
case fps(Int)
case max
public static func <(lhs: FramesPerSecond, rhs: FramesPerSecond) -> Bool {
switch lhs {
case let .fps(lhsFps):
switch rhs {
case let .fps(rhsFps):
return lhsFps < rhsFps
case .max:
return true
}
case .max:
return false
}
}
}
public typealias Link = SharedDisplayLinkDriverLink
public static let shared = SharedDisplayLinkDriver()
public final class LinkImpl: Link {
private let driver: SharedDisplayLinkDriver
public let framesPerSecond: FramesPerSecond
let update: (CGFloat) -> Void
var isValid: Bool = true
public var isPaused: Bool = false {
didSet {
if self.isPaused != oldValue {
self.driver.requestUpdate()
}
}
}
init(driver: SharedDisplayLinkDriver, framesPerSecond: FramesPerSecond, update: @escaping (CGFloat) -> Void) {
self.driver = driver
self.framesPerSecond = framesPerSecond
self.update = update
}
public func invalidate() {
self.isValid = false
}
}
private final class RequestContext {
weak var link: LinkImpl?
let framesPerSecond: FramesPerSecond
var lastDuration: Double = 0.0
init(link: LinkImpl, framesPerSecond: FramesPerSecond) {
self.link = link
self.framesPerSecond = framesPerSecond
}
}
private var displayLink: CADisplayLink?
private var requests: [RequestContext] = []
private var isInForeground: Bool = false
private var isProcessingEvent: Bool = false
private var isUpdateRequested: Bool = false
private init() {
let _ = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: nil, using: { [weak self] _ in
guard let self else {
return
}
self.isInForeground = true
self.update()
})
let _ = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil, using: { [weak self] _ in
guard let self else {
return
}
self.isInForeground = false
self.update()
})
if Bundle.main.bundlePath.hasSuffix(".appex") {
self.isInForeground = true
} else {
switch UIApplication.shared.applicationState {
case .active:
self.isInForeground = true
default:
self.isInForeground = false
}
}
self.update()
}
public func updateForegroundState(_ isActive: Bool) {
if self.isInForeground != isActive {
self.isInForeground = isActive
self.update()
}
}
private func requestUpdate() {
if self.isProcessingEvent {
self.isUpdateRequested = true
} else {
self.update()
}
}
private func update() {
var hasActiveItems = false
var maxFramesPerSecond: FramesPerSecond = .fps(30)
for request in self.requests {
if let link = request.link {
if link.framesPerSecond > maxFramesPerSecond {
maxFramesPerSecond = link.framesPerSecond
}
if link.isValid && !link.isPaused {
hasActiveItems = true
break
}
}
}
if self.isInForeground && hasActiveItems {
let displayLink: CADisplayLink
if let current = self.displayLink {
displayLink = current
} else {
displayLink = CADisplayLink(target: self, selector: #selector(self.displayLinkEvent))
self.displayLink = displayLink
displayLink.add(to: .main, forMode: .common)
}
if #available(iOS 15.0, *) {
let maxFps = Float(UIScreen.main.maximumFramesPerSecond)
if maxFps > 61.0 {
var frameRateRange: CAFrameRateRange
switch maxFramesPerSecond {
case let .fps(fps):
if fps > 60 {
frameRateRange = CAFrameRateRange(minimum: 30.0, maximum: 120.0, preferred: 120.0)
} else {
frameRateRange = .default
}
case .max:
frameRateRange = CAFrameRateRange(minimum: 30.0, maximum: 120.0, preferred: 120.0)
}
if isIpad {
frameRateRange = CAFrameRateRange(minimum: 30.0, maximum: 120.0, preferred: 120.0)
}
if displayLink.preferredFrameRateRange != frameRateRange {
displayLink.preferredFrameRateRange = frameRateRange
print("SharedDisplayLinkDriver: switch to \(frameRateRange)")
}
}
}
displayLink.isPaused = false
} else {
if let displayLink = self.displayLink {
self.displayLink = nil
displayLink.invalidate()
}
}
}
@objc private func displayLinkEvent(displayLink: CADisplayLink) {
self.isProcessingEvent = true
let duration = displayLink.targetTimestamp - displayLink.timestamp
var removeIndices: [Int]?
loop: for i in 0 ..< self.requests.count {
let request = self.requests[i]
if let link = request.link, link.isValid {
if !link.isPaused {
var itemDuration = duration
switch request.framesPerSecond {
case let .fps(value):
let secondsPerFrame = 1.0 / CGFloat(value)
itemDuration = secondsPerFrame
request.lastDuration += duration
if request.lastDuration >= secondsPerFrame * 0.95 {
//print("item \(link) accepting cycle: \(request.lastDuration - duration) + \(duration) = \(request.lastDuration) >= \(secondsPerFrame)")
} else {
//print("item \(link) skipping cycle: \(request.lastDuration - duration) + \(duration) < \(secondsPerFrame)")
continue loop
}
case .max:
break
}
request.lastDuration = 0.0
link.update(itemDuration)
}
} else {
if removeIndices == nil {
removeIndices = [i]
} else {
removeIndices?.append(i)
}
}
}
if let removeIndices = removeIndices {
for index in removeIndices.reversed() {
self.requests.remove(at: index)
}
if self.requests.isEmpty {
self.isUpdateRequested = true
}
}
self.isProcessingEvent = false
if self.isUpdateRequested {
self.isUpdateRequested = false
self.update()
}
}
public func add(framesPerSecond: FramesPerSecond = .fps(60), _ update: @escaping (CGFloat) -> Void) -> Link {
let link = LinkImpl(driver: self, framesPerSecond: framesPerSecond, update: update)
self.requests.append(RequestContext(link: link, framesPerSecond: framesPerSecond))
self.update()
return link
}
}
public final class DisplayLinkTarget: NSObject {
private let f: (CADisplayLink) -> Void
public init(_ f: @escaping (CADisplayLink) -> Void) {
self.f = f
}
@objc public func event(_ displayLink: CADisplayLink) {
self.f(displayLink)
}
}
public final class DisplayLinkAnimator {
private var displayLink: SharedDisplayLinkDriver.Link?
private let duration: Double
private let fromValue: CGFloat
private let toValue: CGFloat
private let startTime: Double
private let update: (CGFloat) -> Void
private let completion: () -> Void
private var completed = false
public init(duration: Double, from fromValue: CGFloat, to toValue: CGFloat, update: @escaping (CGFloat) -> Void, completion: @escaping () -> Void) {
self.duration = duration
self.fromValue = fromValue
self.toValue = toValue
self.update = update
self.completion = completion
self.startTime = CACurrentMediaTime()
self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] _ in
self?.tick()
}
self.displayLink?.isPaused = false
}
deinit {
self.displayLink?.isPaused = true
self.displayLink?.invalidate()
}
public func invalidate() {
self.displayLink?.isPaused = true
self.displayLink?.invalidate()
}
@objc private func tick() {
if self.completed {
return
}
let timestamp = CACurrentMediaTime()
var t = (timestamp - self.startTime) / self.duration
t = max(0.0, t)
t = min(1.0, t)
self.update(self.fromValue * CGFloat(1 - t) + self.toValue * CGFloat(t))
if abs(t - 1.0) < Double.ulpOfOne {
self.completed = true
self.displayLink?.isPaused = true
self.completion()
}
}
}
public final class ConstantDisplayLinkAnimator {
private var displayLink: SharedDisplayLinkDriver.Link?
private let update: () -> Void
private var completed = false
public var frameInterval: Int = 1 {
didSet {
self.updateDisplayLink()
}
}
private func updateDisplayLink() {
guard let displayLink = self.displayLink else {
return
}
let _ = displayLink
}
public var isPaused: Bool = true {
didSet {
if self.isPaused != oldValue {
if !self.isPaused && self.displayLink == nil {
let displayLink = SharedDisplayLinkDriver.shared.add { [weak self] _ in
self?.tick()
}
self.displayLink = displayLink
self.updateDisplayLink()
}
self.displayLink?.isPaused = self.isPaused
}
}
}
public init(update: @escaping () -> Void) {
self.update = update
}
deinit {
if let displayLink = self.displayLink {
displayLink.isPaused = true
displayLink.invalidate()
}
}
public func invalidate() {
if let displayLink = self.displayLink {
displayLink.isPaused = true
displayLink.invalidate()
}
}
@objc private func tick() {
if self.completed {
return
}
self.update()
}
}
@@ -0,0 +1,21 @@
import Foundation
import UIKit
public class DisplayLinkDispatcher: NSObject {
private var blocksToDispatch: [() -> Void] = []
private let limit: Int
public init(limit: Int = 0) {
self.limit = limit
super.init()
}
public func dispatch(f: @escaping () -> Void) {
if Thread.isMainThread {
f()
} else {
DispatchQueue.main.async(execute: f)
}
}
}
@@ -0,0 +1,67 @@
import Foundation
import UIKit
import AsyncDisplayKit
open class EditableTextNode: ASEditableTextNode {
override public var keyboardAppearance: UIKeyboardAppearance {
get {
return super.keyboardAppearance
}
set {
guard newValue != self.keyboardAppearance else {
return
}
super.keyboardAppearance = newValue
self.textView.reloadInputViews()
}
}
public var isRTL: Bool {
if let text = self.textView.text, !text.isEmpty {
let tagger = NSLinguisticTagger(tagSchemes: [.language], options: 0)
tagger.string = text
let lang = tagger.tag(at: 0, scheme: .language, tokenRange: nil, sentenceRange: nil)
if let lang = lang?.rawValue, lang.contains("he") || lang.contains("ar") || lang.contains("fa") {
return true
} else {
return false
}
} else {
return false
}
}
}
public extension UITextView {
var numberOfLines: Int {
let layoutManager = self.layoutManager
let numberOfGlyphs = layoutManager.numberOfGlyphs
var lineRange: NSRange = NSMakeRange(0, 1)
var index = 0
var numberOfLines = 0
while index < numberOfGlyphs {
layoutManager.lineFragmentRect(forGlyphAt: index, effectiveRange: &lineRange)
index = NSMaxRange(lineRange)
numberOfLines += 1
}
return numberOfLines
}
var isRTL: Bool {
if let text = self.text, !text.isEmpty {
let tagger = NSLinguisticTagger(tagSchemes: [.language], options: 0)
tagger.string = text
let lang = tagger.tag(at: 0, scheme: .language, tokenRange: nil, sentenceRange: nil)
if let lang = lang?.rawValue, lang.contains("he") || lang.contains("ar") || lang.contains("fa") {
return true
} else {
return false
}
} else {
return false
}
}
}
+354
View File
@@ -0,0 +1,354 @@
import Foundation
import UIKit
public struct Font {
public enum Design {
case regular
case serif
case monospace
case round
case camera
var key: String {
switch self {
case .regular:
return "regular"
case .serif:
return "serif"
case .monospace:
return "monospace"
case .round:
return "round"
case .camera:
return "camera"
}
}
}
public struct Traits: OptionSet {
public var rawValue: Int32
public init(rawValue: Int32) {
self.rawValue = rawValue
}
public init() {
self.rawValue = 0
}
public static let italic = Traits(rawValue: 1 << 0)
public static let monospacedNumbers = Traits(rawValue: 1 << 1)
}
public enum Width {
case standard
case condensed
case compressed
case expanded
@available(iOS 16.0, *)
var width: UIFont.Width {
switch self {
case .standard:
return .standard
case .condensed:
return .condensed
case .compressed:
return .compressed
case .expanded:
return .expanded
}
}
var key: String {
switch self {
case .standard:
return "standard"
case .condensed:
return "condensed"
case .compressed:
return "compressed"
case .expanded:
return "expanded"
}
}
}
public enum Weight {
case regular
case thin
case light
case medium
case semibold
case bold
case heavy
var isBold: Bool {
switch self {
case .medium, .semibold, .bold, .heavy:
return true
default:
return false
}
}
var weight: UIFont.Weight {
switch self {
case .thin:
return .thin
case .light:
return .light
case .medium:
return .medium
case .semibold:
return .semibold
case .bold:
return .bold
case .heavy:
return .heavy
default:
return .regular
}
}
var key: String {
switch self {
case .regular:
return "regular"
case .thin:
return "thin"
case .light:
return "light"
case .medium:
return "medium"
case .semibold:
return "semibold"
case .bold:
return "bold"
case .heavy:
return "heavy"
}
}
}
private final class Cache {
private var lock: pthread_rwlock_t
private var fonts: [String: UIFont] = [:]
init() {
self.lock = pthread_rwlock_t()
let status = pthread_rwlock_init(&self.lock, nil)
assert(status == 0)
}
func get(_ key: String) -> UIFont? {
let font: UIFont?
pthread_rwlock_rdlock(&self.lock)
font = self.fonts[key]
pthread_rwlock_unlock(&self.lock)
return font
}
func set(_ font: UIFont, key: String) {
pthread_rwlock_wrlock(&self.lock)
self.fonts[key] = font
pthread_rwlock_unlock(&self.lock)
}
}
private static let cache = Cache()
public static func with(size: CGFloat, design: Design = .regular, weight: Weight = .regular, width: Width = .standard, traits: Traits = []) -> UIFont {
let key = "\(size)_\(design.key)_\(weight.key)_\(width.key)_\(traits.rawValue)"
if let cachedFont = self.cache.get(key) {
return cachedFont
}
if #available(iOS 13.0, *), design != .camera {
let descriptor: UIFontDescriptor
if #available(iOS 14.0, *) {
descriptor = UIFont.systemFont(ofSize: size).fontDescriptor
} else {
descriptor = UIFont.systemFont(ofSize: size, weight: weight.weight).fontDescriptor
}
var symbolicTraits = descriptor.symbolicTraits
if traits.contains(.italic) {
symbolicTraits.insert(.traitItalic)
}
var updatedDescriptor: UIFontDescriptor? = descriptor.withSymbolicTraits(symbolicTraits)
if traits.contains(.monospacedNumbers) {
updatedDescriptor = updatedDescriptor?.addingAttributes([
UIFontDescriptor.AttributeName.featureSettings: [
[UIFontDescriptor.FeatureKey.featureIdentifier:
kNumberSpacingType,
UIFontDescriptor.FeatureKey.typeIdentifier:
kMonospacedNumbersSelector]
]])
}
switch design {
case .serif:
updatedDescriptor = updatedDescriptor?.withDesign(.serif)
case .monospace:
updatedDescriptor = updatedDescriptor?.withDesign(.monospaced)
case .round:
updatedDescriptor = updatedDescriptor?.withDesign(.rounded)
default:
updatedDescriptor = updatedDescriptor?.withDesign(.default)
}
if #available(iOS 14.0, *) {
if weight != .regular {
updatedDescriptor = updatedDescriptor?.addingAttributes([
UIFontDescriptor.AttributeName.traits: [UIFontDescriptor.TraitKey.weight: weight.weight]
])
}
}
if #available(iOS 16.0, *) {
if width != .standard {
updatedDescriptor = updatedDescriptor?.addingAttributes([
UIFontDescriptor.AttributeName.traits: [UIFontDescriptor.TraitKey.width: width.width]
])
}
}
let font: UIFont
if let updatedDescriptor = updatedDescriptor {
font = UIFont(descriptor: updatedDescriptor, size: size)
} else {
font = UIFont(descriptor: descriptor, size: size)
}
self.cache.set(font, key: key)
return font
} else {
let font: UIFont
switch design {
case .regular:
if traits.contains(.italic) {
if let descriptor = UIFont.systemFont(ofSize: size, weight: weight.weight).fontDescriptor.withSymbolicTraits([.traitItalic]) {
font = UIFont(descriptor: descriptor, size: size)
} else {
font = UIFont.italicSystemFont(ofSize: size)
}
} else {
return UIFont.systemFont(ofSize: size, weight: weight.weight)
}
case .serif:
if weight.isBold && traits.contains(.italic) {
font = UIFont(name: "Georgia-BoldItalic", size: size - 1.0) ?? UIFont.systemFont(ofSize: size)
} else if weight.isBold {
font = UIFont(name: "Georgia-Bold", size: size - 1.0) ?? UIFont.systemFont(ofSize: size)
} else if traits.contains(.italic) {
font = UIFont(name: "Georgia-Italic", size: size - 1.0) ?? UIFont.systemFont(ofSize: size)
} else {
font = UIFont(name: "Georgia", size: size - 1.0) ?? UIFont.systemFont(ofSize: size)
}
case .monospace:
if weight.isBold && traits.contains(.italic) {
font = UIFont(name: "Menlo-BoldItalic", size: size - 1.0) ?? UIFont.systemFont(ofSize: size)
} else if weight.isBold {
font = UIFont(name: "Menlo-Bold", size: size - 1.0) ?? UIFont.systemFont(ofSize: size)
} else if traits.contains(.italic) {
font = UIFont(name: "Menlo-Italic", size: size - 1.0) ?? UIFont.systemFont(ofSize: size)
} else {
font = UIFont(name: "Menlo", size: size - 1.0) ?? UIFont.systemFont(ofSize: size)
}
case .round:
font = UIFont(name: ".SFCompactRounded-Semibold", size: size) ?? UIFont.systemFont(ofSize: size)
case .camera:
func encodeText(string: String, key: Int16) -> String {
let nsString = string as NSString
let result = NSMutableString()
for i in 0 ..< nsString.length {
var c: unichar = nsString.character(at: i)
c = unichar(Int16(c) + key)
result.append(NSString(characters: &c, length: 1) as String)
}
return result as String
}
if case .semibold = weight {
font = UIFont(name: encodeText(string: "TGDbnfsb.Tfnjcpme", key: -1), size: size) ?? UIFont.systemFont(ofSize: size, weight: weight.weight)
} else {
font = UIFont(name: encodeText(string: "TGDbnfsb.Sfhvmbs", key: -1), size: size) ?? UIFont.systemFont(ofSize: size, weight: weight.weight)
}
}
self.cache.set(font, key: key)
return font
}
}
public static func regular(_ size: CGFloat) -> UIFont {
return UIFont.systemFont(ofSize: size)
}
public static func medium(_ size: CGFloat) -> UIFont {
return UIFont.systemFont(ofSize: size, weight: UIFont.Weight.medium)
}
public static func semibold(_ size: CGFloat) -> UIFont {
return UIFont.systemFont(ofSize: size, weight: UIFont.Weight.semibold)
}
public static func bold(_ size: CGFloat) -> UIFont {
if #available(iOS 8.2, *) {
return UIFont.boldSystemFont(ofSize: size)
} else {
return CTFontCreateWithName("HelveticaNeue-Bold" as CFString, size, nil)
}
}
public static func heavy(_ size: CGFloat) -> UIFont {
return self.with(size: size, design: .regular, weight: .heavy, traits: [])
}
public static func light(_ size: CGFloat) -> UIFont {
return UIFont.systemFont(ofSize: size, weight: UIFont.Weight.light)
}
public static func semiboldItalic(_ size: CGFloat) -> UIFont {
if let descriptor = UIFont.systemFont(ofSize: size).fontDescriptor.withSymbolicTraits([.traitBold, .traitItalic]) {
return UIFont(descriptor: descriptor, size: size)
} else {
return UIFont.italicSystemFont(ofSize: size)
}
}
public static func monospace(_ size: CGFloat) -> UIFont {
return UIFont(name: "Menlo-Regular", size: size - 1.0) ?? UIFont.systemFont(ofSize: size)
}
public static func semiboldMonospace(_ size: CGFloat) -> UIFont {
return UIFont(name: "Menlo-Bold", size: size - 1.0) ?? UIFont.systemFont(ofSize: size)
}
public static func italicMonospace(_ size: CGFloat) -> UIFont {
return UIFont(name: "Menlo-Italic", size: size - 1.0) ?? UIFont.systemFont(ofSize: size)
}
public static func semiboldItalicMonospace(_ size: CGFloat) -> UIFont {
return UIFont(name: "Menlo-BoldItalic", size: size - 1.0) ?? UIFont.systemFont(ofSize: size)
}
public static func italic(_ size: CGFloat) -> UIFont {
return UIFont.italicSystemFont(ofSize: size)
}
}
public extension NSAttributedString {
convenience init(string: String, font: UIFont? = nil, textColor: UIColor = UIColor.black, paragraphAlignment: NSTextAlignment? = nil) {
var attributes: [NSAttributedString.Key: AnyObject] = [:]
if let font = font {
attributes[NSAttributedString.Key.font] = font
}
attributes[NSAttributedString.Key.foregroundColor] = textColor
if let paragraphAlignment = paragraphAlignment {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = paragraphAlignment
attributes[NSAttributedString.Key.paragraphStyle] = paragraphStyle
}
self.init(string: string, attributes: attributes)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,213 @@
import Foundation
import UIKit
import ObjectiveC
import AsyncDisplayKit
private var ASGestureRecognizerDelegateKey: Int?
private var ASScrollViewDelegateKey: Int?
private final class WrappedGestureRecognizerDelegate: NSObject, UIGestureRecognizerDelegate {
private weak var target: ASGestureRecognizerDelegate?
init(target: ASGestureRecognizerDelegate) {
self.target = target
super.init()
}
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard let target = self.target else {
return true
}
return target.gestureRecognizerShouldBegin?(gestureRecognizer) ?? true
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
guard let target = self.target else {
return false
}
return target.gestureRecognizer?(gestureRecognizer, shouldRecognizeSimultaneouslyWith: otherGestureRecognizer) ?? false
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
guard let target = self.target else {
return false
}
return target.gestureRecognizer?(gestureRecognizer, shouldRequireFailureOf: otherGestureRecognizer) ?? false
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
guard let target = self.target else {
return false
}
return target.gestureRecognizer?(gestureRecognizer, shouldBeRequiredToFailBy: otherGestureRecognizer) ?? false
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
guard let target = self.target else {
return true
}
return target.gestureRecognizer?(gestureRecognizer, shouldReceive: touch) ?? true
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive press: UIPress) -> Bool {
guard let target = self.target else {
return true
}
return target.gestureRecognizer?(gestureRecognizer, shouldReceive: press) ?? true
}
@available(iOS 13.4, *)
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive event: UIEvent) -> Bool {
guard let target = self.target else {
return true
}
return target.gestureRecognizer?(gestureRecognizer, shouldReceive: event) ?? true
}
}
public extension ASGestureRecognizerDelegate {
var wrappedGestureRecognizerDelegate: UIGestureRecognizerDelegate {
if let delegate = objc_getAssociatedObject(self, &ASGestureRecognizerDelegateKey) as? WrappedGestureRecognizerDelegate {
return delegate
} else {
let delegate = WrappedGestureRecognizerDelegate(target: self)
objc_setAssociatedObject(self, &ASGestureRecognizerDelegateKey, delegate, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return delegate
}
}
}
private final class WrappedScrollViewDelegate: NSObject, UIScrollViewDelegate, UIScrollViewAccessibilityDelegate {
private weak var target: ASScrollViewDelegate?
init(target: ASScrollViewDelegate) {
self.target = target
super.init()
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let target = self.target else {
return
}
target.scrollViewDidScroll?(scrollView)
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
guard let target = self.target else {
return
}
target.scrollViewDidZoom?(scrollView)
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
guard let target = self.target else {
return
}
target.scrollViewWillBeginDragging?(scrollView)
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
guard let target = self.target else {
return
}
target.scrollViewWillEndDragging?(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset)
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
guard let target = self.target else {
return
}
target.scrollViewDidEndDragging?(scrollView, willDecelerate: decelerate)
}
func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
guard let target = self.target else {
return
}
target.scrollViewWillBeginDecelerating?(scrollView)
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
guard let target = self.target else {
return
}
target.scrollViewDidEndDecelerating?(scrollView)
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
guard let target = self.target else {
return
}
target.scrollViewDidEndScrollingAnimation?(scrollView)
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
guard let target = self.target else {
return nil
}
return target.viewForZooming?(in: scrollView)
}
func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
guard let target = self.target else {
return
}
target.scrollViewWillBeginZooming?(scrollView, with: view)
}
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
guard let target = self.target else {
return
}
target.scrollViewDidEndZooming?(scrollView, with: view, atScale: scale)
}
func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
guard let target = self.target else {
return true
}
return target.scrollViewShouldScroll?(toTop: scrollView) ?? true
}
func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
guard let target = self.target else {
return
}
target.scrollViewDidScroll?(toTop: scrollView)
}
func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView) {
guard let target = self.target else {
return
}
target.scrollViewDidChangeAdjustedContentInset?(scrollView)
}
func accessibilityScrollStatus(for scrollView: UIScrollView) -> String? {
guard let target = self.target else {
return nil
}
return target.accessibilityScrollStatus?(for: scrollView)
}
func accessibilityAttributedScrollStatus(for scrollView: UIScrollView) -> NSAttributedString? {
guard let target = self.target else {
return nil
}
return target.accessibilityAttributedScrollStatus?(for: scrollView)
}
}
public extension ASScrollViewDelegate {
var wrappedScrollViewDelegate: UIScrollViewDelegate & UIScrollViewAccessibilityDelegate {
if let delegate = objc_getAssociatedObject(self, &ASScrollViewDelegateKey) as? WrappedScrollViewDelegate {
return delegate
} else {
let delegate = WrappedScrollViewDelegate(target: self)
objc_setAssociatedObject(self, &ASScrollViewDelegateKey, delegate, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return delegate
}
}
}
@@ -0,0 +1,314 @@
import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
public func isViewVisibleInHierarchy(_ view: UIView, _ initial: Bool = true) -> Bool {
guard let window = view.window else {
return false
}
if view.isHidden || view.alpha == 0.0 {
return false
}
if view.superview === window {
return true
} else if let superview = view.superview {
if initial && view.frame.minY >= superview.frame.height {
return false
} else {
return isViewVisibleInHierarchy(superview, false)
}
} else {
return false
}
}
public final class HierarchyTrackingNode: ASDisplayNode {
public var updated: (Bool) -> Void
public init(_ f: @escaping (Bool) -> Void = { _ in }) {
self.updated = f
super.init()
self.isLayerBacked = true
}
override public func didEnterHierarchy() {
super.didEnterHierarchy()
self.updated(true)
}
override public func didExitHierarchy() {
super.didExitHierarchy()
self.updated(false)
}
}
final class GlobalOverlayPresentationContext {
private let statusBarHost: StatusBarHost?
private weak var parentView: UIView?
private(set) var controllers: [ContainableController] = []
private var globalPortalViews: [GlobalPortalView] = []
private var presentationDisposables = DisposableSet()
private var layout: ContainerViewLayout?
private var ready: Bool {
return self.currentPresentationView(underStatusBar: false) != nil && self.layout != nil
}
init(statusBarHost: StatusBarHost?, parentView: UIView) {
self.statusBarHost = statusBarHost
self.parentView = parentView
}
private var currentTrackingNode: HierarchyTrackingNode?
private func currentPresentationView(underStatusBar: Bool) -> UIView? {
if let statusBarHost = self.statusBarHost {
if let keyboardWindow = statusBarHost.keyboardWindow, let keyboardView = statusBarHost.keyboardView, !keyboardView.frame.height.isZero, isViewVisibleInHierarchy(keyboardView) {
var updateTrackingNode = false
if let trackingNode = self.currentTrackingNode {
if trackingNode.layer.superlayer !== keyboardView.layer {
updateTrackingNode = true
}
} else {
updateTrackingNode = true
}
if updateTrackingNode {
/*self.currentTrackingNode?.removeFromSupernode()
let trackingNode = HierarchyTrackingNode({ [weak self] value in
guard let strongSelf = self else {
return
}
if !value {
strongSelf.addViews(justMove: true)
}
})
self.currentTrackingNode = trackingNode
keyboardView.layer.addSublayer(trackingNode.layer)*/
}
return keyboardWindow
} else {
if let view = self.parentView {
return view
}
}
}
return nil
}
func present(_ controller: ContainableController) {
let controllerReady = controller.ready.get()
|> filter({ $0 })
|> take(1)
|> deliverOnMainQueue
|> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(true))
var underStatusBar = false
if let controller = controller as? ViewController {
if case .Hide = controller.statusBar.statusBarStyle {
underStatusBar = true
}
}
if let presentationView = self.currentPresentationView(underStatusBar: underStatusBar), let initialLayout = self.layout {
if initialLayout.metrics.widthClass == .regular {
controller.view.frame = CGRect(origin: CGPoint(x: presentationView.bounds.width - initialLayout.size.width, y: 0.0), size: initialLayout.size)
} else {
controller.view.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: initialLayout.size)
}
controller.containerLayoutUpdated(initialLayout, transition: .immediate)
self.presentationDisposables.add(controllerReady.start(next: { [weak self] _ in
if let strongSelf = self {
if strongSelf.controllers.contains(where: { $0 === controller }) {
return
}
strongSelf.controllers.append(controller)
if let view = strongSelf.currentPresentationView(underStatusBar: underStatusBar), let layout = strongSelf.layout {
(controller as? UIViewController)?.navigation_setDismiss({ [weak controller] in
if let strongSelf = self, let controller = controller {
strongSelf.dismiss(controller)
}
}, rootController: nil)
(controller as? UIViewController)?.setIgnoreAppearanceMethodInvocations(true)
if layout != initialLayout {
if layout.metrics.widthClass == .regular {
controller.view.frame = CGRect(origin: CGPoint(x: view.bounds.width - layout.size.width, y: 0.0), size: layout.size)
} else {
controller.view.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: layout.size)
}
view.addSubview(controller.view)
controller.containerLayoutUpdated(layout, transition: .immediate)
} else {
view.addSubview(controller.view)
}
(controller as? UIViewController)?.setIgnoreAppearanceMethodInvocations(false)
controller.viewWillAppear(false)
controller.viewDidAppear(false)
}
}
}))
} else {
self.controllers.append(controller)
}
}
deinit {
self.presentationDisposables.dispose()
}
private func dismiss(_ controller: ContainableController) {
if let index = self.controllers.firstIndex(where: { $0 === controller }) {
self.controllers.remove(at: index)
controller.viewWillDisappear(false)
controller.view.removeFromSuperview()
controller.viewDidDisappear(false)
}
}
public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
let wasReady = self.ready
self.layout = layout
if wasReady != self.ready {
self.readyChanged(wasReady: wasReady)
} else if self.ready {
for controller in self.controllers {
transition.updateFrame(node: controller.displayNode, frame: CGRect(origin: CGPoint(), size: layout.size))
controller.containerLayoutUpdated(layout, transition: transition)
}
for globalPortalView in self.globalPortalViews {
transition.updateFrame(view: globalPortalView.view, frame: CGRect(origin: CGPoint(), size: layout.size))
}
}
}
public func addGlobalPortalHostView(sourceView: PortalSourceView) {
guard let globalPortalView = GlobalPortalView(wasRemoved: { [weak self] globalPortalView in
guard let strongSelf = self else {
return
}
if let index = strongSelf.globalPortalViews.firstIndex(where: { $0 === globalPortalView }) {
strongSelf.globalPortalViews.remove(at: index)
}
globalPortalView.view.removeFromSuperview()
}) else {
return
}
globalPortalView.view.isUserInteractionEnabled = false
self.globalPortalViews.append(globalPortalView)
sourceView.setGlobalPortal(view: globalPortalView)
if let presentationView = self.currentPresentationView(underStatusBar: true), let initialLayout = self.layout {
presentationView.addSubview(globalPortalView.view)
globalPortalView.view.frame = CGRect(origin: CGPoint(), size: initialLayout.size)
}
}
private func readyChanged(wasReady: Bool) {
if !wasReady {
self.addViews(justMove: false)
} else {
self.removeViews()
}
}
private func addViews(justMove: Bool) {
if let layout = self.layout {
for controller in self.controllers {
var underStatusBar = false
if let controller = controller as? ViewController {
if case .Hide = controller.statusBar.statusBarStyle {
underStatusBar = true
}
}
if let view = self.currentPresentationView(underStatusBar: underStatusBar) {
if !justMove {
controller.viewWillAppear(false)
}
view.addSubview(controller.view)
if !justMove {
if layout.metrics.widthClass == .regular {
controller.view.frame = CGRect(origin: CGPoint(x: view.bounds.width - layout.size.width, y: 0.0), size: layout.size)
} else {
controller.view.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: layout.size)
}
controller.containerLayoutUpdated(layout, transition: .immediate)
controller.viewDidAppear(false)
}
}
}
if !self.globalPortalViews.isEmpty, let view = self.currentPresentationView(underStatusBar: true) {
for globalPortalView in self.globalPortalViews {
view.addSubview(globalPortalView.view)
globalPortalView.view.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: layout.size)
}
}
}
}
private func removeViews() {
for controller in self.controllers {
controller.viewWillDisappear(false)
controller.view.removeFromSuperview()
controller.viewDidDisappear(false)
}
for globalPortalView in self.globalPortalViews {
globalPortalView.view.removeFromSuperview()
}
}
func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
for controller in self.controllers.reversed() {
if controller.isViewLoaded {
if let result = controller.view.hitTest(point, with: event) {
return result
}
}
}
return nil
}
func updateToInterfaceOrientation(_ orientation: UIInterfaceOrientation) {
if self.ready {
for controller in self.controllers {
controller.updateToInterfaceOrientation(orientation)
}
}
}
func combinedSupportedOrientations(currentOrientationToLock: UIInterfaceOrientationMask) -> ViewControllerSupportedOrientations {
var mask = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .all)
for controller in self.controllers {
mask = mask.intersection(controller.combinedSupportedOrientations(currentOrientationToLock: currentOrientationToLock))
}
return mask
}
func combinedDeferScreenEdgeGestures() -> UIRectEdge {
var edges: UIRectEdge = []
for controller in self.controllers {
edges = edges.union(controller.deferScreenEdgeGestures)
}
return edges
}
}
@@ -0,0 +1,19 @@
import UIKit
final class GlobalPortalView: PortalView {
private let wasRemoved: (GlobalPortalView) -> Void
init?(wasRemoved: @escaping (GlobalPortalView) -> Void) {
self.wasRemoved = wasRemoved
super.init()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func triggerWasRemoved() {
self.wasRemoved(self)
}
}
+39
View File
@@ -0,0 +1,39 @@
import Foundation
import UIKit
import AsyncDisplayKit
public protocol GridSection {
var height: CGFloat { get }
var hashValue: Int { get }
func isEqual(to: GridSection) -> Bool
func node() -> ASDisplayNode
}
public protocol GridItem {
var section: GridSection? { get }
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode
func update(node: GridItemNode)
var aspectRatio: CGFloat { get }
var fillsRowWithHeight: (CGFloat, Bool)? { get }
var fillsRowWithDynamicHeight: ((CGFloat) -> CGFloat)? { get }
var customItemSize: CGSize? { get }
}
public extension GridItem {
var aspectRatio: CGFloat {
return 1.0
}
var fillsRowWithHeight: (CGFloat, Bool)? {
return nil
}
var fillsRowWithDynamicHeight: ((CGFloat) -> CGFloat)? {
return nil
}
var customItemSize: CGSize? {
return nil
}
}
@@ -0,0 +1,24 @@
import Foundation
import UIKit
import AsyncDisplayKit
open class GridItemNode: ASDisplayNode {
open var isVisibleInGrid = false
open var isGridScrolling = false
final var cachedFrame: CGRect = CGRect()
override open var frame: CGRect {
get {
return self.cachedFrame
} set(value) {
self.cachedFrame = value
super.frame = value
}
}
open func updateLayout(item: GridItem, size: CGSize, isVisible: Bool, synchronousLoads: Bool) {
}
open func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) {
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,53 @@
import UIKit
import AsyncDisplayKit
private class GridNodeScrollerLayer: CALayer {
override func setNeedsDisplay() {
}
}
public class GridNodeScrollerView: UIScrollView {
override public class var layerClass: AnyClass {
return GridNodeScrollerLayer.self
}
override public init(frame: CGRect) {
super.init(frame: frame)
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.contentInsetAdjustmentBehavior = .never
}
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
@objc private func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
}
open class GridNodeScroller: ASDisplayNode, ASGestureRecognizerDelegate {
public var scrollView: UIScrollView {
return self.view as! UIScrollView
}
override init() {
super.init()
self.setViewBlock({
return GridNodeScrollerView(frame: CGRect())
})
self.scrollView.scrollsToTop = false
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
@@ -0,0 +1,240 @@
import Foundation
import UIKit
import AudioToolbox
import CoreHaptics
public enum ImpactHapticFeedbackStyle: Hashable {
case light
case medium
case heavy
case soft
case rigid
case veryLight
case click05
case click06
}
@available(iOSApplicationExtension 10.0, iOS 10.0, *)
private final class HapticFeedbackImpl {
private lazy var impactGenerator: [ImpactHapticFeedbackStyle : UIImpactFeedbackGenerator] = {
if #available(iOSApplicationExtension 13.0, iOS 13.0, *) {
return [.light: UIImpactFeedbackGenerator(style: .light),
.medium: UIImpactFeedbackGenerator(style: .medium),
.heavy: UIImpactFeedbackGenerator(style: .heavy),
.soft: UIImpactFeedbackGenerator(style: .soft),
.rigid: UIImpactFeedbackGenerator(style: .rigid),
.veryLight: UIImpactFeedbackGenerator(),
.click05: UIImpactFeedbackGenerator(),
.click06: UIImpactFeedbackGenerator()]
} else {
return [.light: UIImpactFeedbackGenerator(style: .light),
.medium: UIImpactFeedbackGenerator(style: .medium),
.heavy: UIImpactFeedbackGenerator(style: .heavy)]
}
}()
private lazy var selectionGenerator: UISelectionFeedbackGenerator? = {
return UISelectionFeedbackGenerator()
}()
private lazy var notificationGenerator: UINotificationFeedbackGenerator? = {
return UINotificationFeedbackGenerator()
}()
func prepareTap() {
if let selectionGenerator = self.selectionGenerator {
selectionGenerator.prepare()
}
}
func tap() {
if let selectionGenerator = self.selectionGenerator {
selectionGenerator.selectionChanged()
}
}
func prepareImpact(_ style: ImpactHapticFeedbackStyle) {
if let impactGenerator = self.impactGenerator[style] {
impactGenerator.prepare()
}
}
func impact(_ style: ImpactHapticFeedbackStyle) {
if let impactGenerator = self.impactGenerator[style] {
if #available(iOSApplicationExtension 13.0, iOS 13.0, *) {
switch style {
case .click05:
impactGenerator.impactOccurred(intensity: 0.3)
case .click06:
impactGenerator.impactOccurred(intensity: 0.4)
case .veryLight:
impactGenerator.impactOccurred(intensity: 0.3)
default:
impactGenerator.impactOccurred()
}
} else {
impactGenerator.impactOccurred()
}
}
}
func success() {
if let notificationGenerator = self.notificationGenerator {
notificationGenerator.notificationOccurred(.success)
} else {
AudioServicesPlaySystemSound(1520)
}
}
func prepareError() {
if let notificationGenerator = self.notificationGenerator {
notificationGenerator.prepare()
}
}
func error() {
if let notificationGenerator = self.notificationGenerator {
notificationGenerator.notificationOccurred(.error)
} else {
AudioServicesPlaySystemSound(1521)
}
}
func warning() {
AudioServicesPlaySystemSound(1102)
// if let notificationGenerator = self.notificationGenerator {
// notificationGenerator.notificationOccurred(.warning)
// } else {
//
// }
}
@objc dynamic func f() {
}
}
public final class HapticFeedback {
private var impl: AnyObject?
public init() {
}
deinit {
let impl = self.impl
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0, execute: {
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
if let impl = impl as? HapticFeedbackImpl {
impl.f()
}
}
})
}
@available(iOSApplicationExtension 10.0, iOS 10.0, *)
private func withImpl(_ f: (HapticFeedbackImpl) -> Void) {
if self.impl == nil {
self.impl = HapticFeedbackImpl()
}
f(self.impl as! HapticFeedbackImpl)
}
public func prepareTap() {
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
self.withImpl { impl in
impl.prepareTap()
}
}
}
public func tap() {
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
self.withImpl { impl in
impl.tap()
}
}
}
public func prepareImpact(_ style: ImpactHapticFeedbackStyle = .medium) {
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
self.withImpl { impl in
impl.prepareImpact(style)
}
}
}
public func impact(_ style: ImpactHapticFeedbackStyle = .medium) {
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
self.withImpl { impl in
impl.impact(style)
}
}
}
public func success() {
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
self.withImpl { impl in
impl.success()
}
}
}
public func prepareError() {
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
self.withImpl { impl in
impl.prepareError()
}
}
}
public func error() {
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
self.withImpl { impl in
impl.error()
}
}
}
public func warning() {
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
self.withImpl { impl in
impl.warning()
}
}
}
}
@available(iOS 13.0, *)
public final class ContinuousHaptic {
private let engine: CHHapticEngine
private let player: CHHapticPatternPlayer
public init(duration: Double) throws {
self.engine = try CHHapticEngine()
var events: [CHHapticEvent] = []
for i in 0 ... 10 {
let t = CGFloat(i) / 10.0
let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: Float((1.0 - t) * 0.1 + t * 1.0))
let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
let eventDuration: Double
if i == 10 {
eventDuration = 100.0
} else {
eventDuration = duration
}
let event = CHHapticEvent(eventType: .hapticContinuous, parameters: [intensity, sharpness], relativeTime: Double(i) / 10.0 * duration, duration: eventDuration)
events.append(event)
}
let pattern = try CHHapticPattern(events: events, parameters: [])
self.player = try self.engine.makePlayer(with: pattern)
try self.engine.start()
try self.player.start(atTime: 0)
}
deinit {
self.engine.stop(completionHandler: nil)
}
}
@@ -0,0 +1,56 @@
import UIKit
open class HighlightTrackingButton: UIButton {
private var internalHighlighted = false
public var internalHighligthedChanged: (Bool) -> Void = { _ in }
public var highligthedChanged: (Bool) -> Void = { _ in }
override public init(frame: CGRect) {
super.init(frame: frame)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
open override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
if !self.internalHighlighted {
self.internalHighlighted = true
self.highligthedChanged(true)
self.internalHighligthedChanged(true)
}
return super.beginTracking(touch, with: event)
}
open override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
if self.internalHighlighted {
self.internalHighlighted = false
self.highligthedChanged(false)
self.internalHighligthedChanged(false)
}
super.endTracking(touch, with: event)
}
open override func cancelTracking(with event: UIEvent?) {
if self.internalHighlighted {
self.internalHighlighted = false
self.highligthedChanged(false)
self.internalHighligthedChanged(false)
}
super.cancelTracking(with: event)
}
open override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
if self.internalHighlighted {
self.internalHighlighted = false
self.highligthedChanged(false)
self.internalHighligthedChanged(false)
}
super.touchesCancelled(touches, with: event)
}
}
@@ -0,0 +1,103 @@
import Foundation
import UIKit
import AsyncDisplayKit
open class HighlightableButton: HighlightTrackingButton {
override public init(frame: CGRect) {
super.init(frame: frame)
self.adjustsImageWhenHighlighted = false
self.adjustsImageWhenDisabled = false
self.internalHighligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.layer.removeAnimation(forKey: "opacity")
strongSelf.alpha = 0.4
} else {
strongSelf.alpha = 1.0
strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
open class HighlightTrackingButtonNode: ASButtonNode {
private var internalHighlighted = false
public var highligthedChanged: (Bool) -> Void = { _ in }
private let pointerStyle: PointerStyle?
public var pointerInteraction: PointerInteraction?
public init(pointerStyle: PointerStyle? = nil) {
self.pointerStyle = pointerStyle
super.init()
}
open override func didLoad() {
super.didLoad()
if let pointerStyle = self.pointerStyle {
self.pointerInteraction = PointerInteraction(node: self, style: pointerStyle)
}
}
open override func beginTracking(with touch: UITouch, with event: UIEvent?) -> Bool {
if !self.internalHighlighted {
self.internalHighlighted = true
self.highligthedChanged(true)
}
return super.beginTracking(with: touch, with: event)
}
open override func endTracking(with touch: UITouch?, with event: UIEvent?) {
if self.internalHighlighted {
self.internalHighlighted = false
self.highligthedChanged(false)
}
super.endTracking(with: touch, with: event)
}
open override func cancelTracking(with event: UIEvent?) {
if self.internalHighlighted {
self.internalHighlighted = false
self.highligthedChanged(false)
}
super.cancelTracking(with: event)
}
open override func touchesCancelled(_ touches: Set<UITouch>?, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
if self.internalHighlighted {
self.internalHighlighted = false
self.highligthedChanged(false)
}
}
}
open class HighlightableButtonNode: HighlightTrackingButtonNode {
override public init(pointerStyle: PointerStyle? = nil) {
super.init(pointerStyle: pointerStyle)
self.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self, !strongSelf.isImplicitlyDisabled {
if highlighted {
strongSelf.layer.removeAnimation(forKey: "opacity")
strongSelf.alpha = 0.4
} else {
strongSelf.alpha = 1.0
strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
}
}
@@ -0,0 +1,142 @@
import Foundation
import UIKit
import CoreGraphics
import SwiftSignalKit
private enum Corner: Hashable {
case TopLeft(Int), TopRight(Int), BottomLeft(Int), BottomRight(Int)
var radius: Int {
switch self {
case let .TopLeft(radius):
return radius
case let .TopRight(radius):
return radius
case let .BottomLeft(radius):
return radius
case let .BottomRight(radius):
return radius
}
}
}
private enum Tail: Hashable {
case BottomLeft(Int)
case BottomRight(Int)
var radius: Int {
switch self {
case let .BottomLeft(radius):
return radius
case let .BottomRight(radius):
return radius
}
}
}
private var cachedCorners = Atomic<[Corner: DrawingContext]>(value: [:])
private func cornerContext(_ corner: Corner) -> DrawingContext {
let cached: DrawingContext? = cachedCorners.with {
return $0[corner]
}
if let cached = cached {
return cached
} else {
let context = DrawingContext(size: CGSize(width: CGFloat(corner.radius), height: CGFloat(corner.radius)), clear: true)!
context.withContext { c in
c.clear(CGRect(origin: CGPoint(), size: CGSize(width: CGFloat(corner.radius), height: CGFloat(corner.radius))))
c.setFillColor(UIColor.black.cgColor)
switch corner {
case let .TopLeft(radius):
let rect = CGRect(origin: CGPoint(), size: CGSize(width: CGFloat(radius * 2), height: CGFloat(radius * 2)))
c.fillEllipse(in: rect)
case let .TopRight(radius):
let rect = CGRect(origin: CGPoint(x: -CGFloat(radius), y: 0.0), size: CGSize(width: CGFloat(radius * 2), height: CGFloat(radius * 2)))
c.fillEllipse(in: rect)
case let .BottomLeft(radius):
let rect = CGRect(origin: CGPoint(x: 0.0, y: -CGFloat(radius)), size: CGSize(width: CGFloat(radius * 2), height: CGFloat(radius * 2)))
c.fillEllipse(in: rect)
case let .BottomRight(radius):
let rect = CGRect(origin: CGPoint(x: -CGFloat(radius), y: -CGFloat(radius)), size: CGSize(width: CGFloat(radius * 2), height: CGFloat(radius * 2)))
c.fillEllipse(in: rect)
}
}
let _ = cachedCorners.modify { current in
var current = current
current[corner] = context
return current
}
return context
}
}
public func addCorners(_ context: DrawingContext, arguments: TransformImageArguments) {
let corners = arguments.corners
let drawingRect = arguments.drawingRect
if case let .Corner(radius) = corners.topLeft, radius > CGFloat.ulpOfOne {
let corner = cornerContext(.TopLeft(Int(radius)))
context.blt(corner, at: CGPoint(x: drawingRect.minX, y: drawingRect.minY))
}
if case let .Corner(radius) = corners.topRight, radius > CGFloat.ulpOfOne {
let corner = cornerContext(.TopRight(Int(radius)))
context.blt(corner, at: CGPoint(x: drawingRect.maxX - radius, y: drawingRect.minY))
}
switch corners.bottomLeft {
case let .Corner(radius):
if radius > CGFloat.ulpOfOne {
let corner = cornerContext(.BottomLeft(Int(radius)))
context.blt(corner, at: CGPoint(x: drawingRect.minX, y: drawingRect.maxY - radius))
}
case let .Tail(radius, image):
if radius > CGFloat.ulpOfOne {
let color = context.colorAt(CGPoint(x: drawingRect.minX, y: drawingRect.maxY - 1.0))
context.withContext { c in
c.clear(CGRect(x: drawingRect.minX - 4.0, y: 0.0, width: 4.0, height: drawingRect.maxY - 6.0))
c.setFillColor(color.cgColor)
c.fill(CGRect(x: 0.0, y: drawingRect.maxY - 7.0, width: 4.0, height: 7.0))
c.setBlendMode(.destinationIn)
let cornerRect = CGRect(origin: CGPoint(x: drawingRect.minX - 6.0, y: drawingRect.maxY - image.size.height), size: image.size)
c.translateBy(x: cornerRect.midX, y: cornerRect.midY)
c.scaleBy(x: 1.0, y: -1.0)
c.translateBy(x: -cornerRect.midX, y: -cornerRect.midY)
c.draw(image.cgImage!, in: cornerRect)
c.translateBy(x: cornerRect.midX, y: cornerRect.midY)
c.scaleBy(x: 1.0, y: -1.0)
c.translateBy(x: -cornerRect.midX, y: -cornerRect.midY)
}
}
}
switch corners.bottomRight {
case let .Corner(radius):
if radius > CGFloat.ulpOfOne {
let corner = cornerContext(.BottomRight(Int(radius)))
context.blt(corner, at: CGPoint(x: drawingRect.maxX - radius, y: drawingRect.maxY - radius))
}
case let .Tail(radius, image):
if radius > CGFloat.ulpOfOne {
let color = context.colorAt(CGPoint(x: drawingRect.maxX - 1.0, y: drawingRect.maxY - 1.0))
context.withContext { c in
c.clear(CGRect(x: drawingRect.maxX, y: 0.0, width: 4.0, height: drawingRect.maxY - image.size.height))
c.setFillColor(color.cgColor)
c.fill(CGRect(x: drawingRect.maxX, y: drawingRect.maxY - 7.0, width: 5.0, height: 7.0))
c.setBlendMode(.destinationIn)
let cornerRect = CGRect(origin: CGPoint(x: drawingRect.maxX - image.size.width + 6.0, y: drawingRect.maxY - image.size.height), size: image.size)
c.translateBy(x: cornerRect.midX, y: cornerRect.midY)
c.scaleBy(x: 1.0, y: -1.0)
c.translateBy(x: -cornerRect.midX, y: -cornerRect.midY)
c.draw(image.cgImage!, in: cornerRect)
c.translateBy(x: cornerRect.midX, y: cornerRect.midY)
c.scaleBy(x: 1.0, y: -1.0)
c.translateBy(x: -cornerRect.midX, y: -cornerRect.midY)
}
}
}
}
+238
View File
@@ -0,0 +1,238 @@
import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
public let displayLinkDispatcher = DisplayLinkDispatcher()
private let dispatcher = displayLinkDispatcher
public enum ImageCorner: Equatable {
case Corner(CGFloat)
case Tail(CGFloat, UIImage)
public var extendedInsets: CGSize {
switch self {
case .Tail:
return CGSize(width: 4.0, height: 0.0)
default:
return CGSize()
}
}
public var withoutTail: ImageCorner {
switch self {
case .Corner:
return self
case let .Tail(radius, _):
return .Corner(radius)
}
}
public var radius: CGFloat {
switch self {
case let .Corner(radius):
return radius
case let .Tail(radius, _):
return radius
}
}
}
public func ==(lhs: ImageCorner, rhs: ImageCorner) -> Bool {
switch lhs {
case let .Corner(lhsRadius):
switch rhs {
case let .Corner(rhsRadius) where abs(lhsRadius - rhsRadius) < CGFloat.ulpOfOne:
return true
default:
return false
}
case let .Tail(lhsRadius, lhsImage):
if case let .Tail(rhsRadius, rhsImage) = rhs, lhsRadius.isEqual(to: rhsRadius), lhsImage === rhsImage {
return true
} else {
return false
}
}
}
public func isRoundEqualCorners(_ corners: ImageCorners) -> Bool {
if case .Corner = corners.topLeft, case .Corner = corners.topRight, case .Corner = corners.bottomLeft, case .Corner = corners.bottomRight {
if corners.topLeft.radius == corners.topRight.radius && corners.topRight.radius == corners.bottomLeft.radius && corners.bottomLeft.radius == corners.bottomRight.radius {
return true
}
}
return false
}
public struct ImageCorners: Equatable {
public enum Curve {
case circular
case continuous
}
public let topLeft: ImageCorner
public let topRight: ImageCorner
public let bottomLeft: ImageCorner
public let bottomRight: ImageCorner
public let curve: Curve
public var isEmpty: Bool {
if self.topLeft != .Corner(0.0) {
return false
}
if self.topRight != .Corner(0.0) {
return false
}
if self.bottomLeft != .Corner(0.0) {
return false
}
if self.bottomRight != .Corner(0.0) {
return false
}
return true
}
public init(radius: CGFloat, curve: Curve = .circular) {
self.topLeft = .Corner(radius)
self.topRight = .Corner(radius)
self.bottomLeft = .Corner(radius)
self.bottomRight = .Corner(radius)
self.curve = curve
}
public init(topLeft: ImageCorner, topRight: ImageCorner, bottomLeft: ImageCorner, bottomRight: ImageCorner, curve: Curve = .circular) {
self.topLeft = topLeft
self.topRight = topRight
self.bottomLeft = bottomLeft
self.bottomRight = bottomRight
self.curve = curve
}
public init() {
self.init(topLeft: .Corner(0.0), topRight: .Corner(0.0), bottomLeft: .Corner(0.0), bottomRight: .Corner(0.0), curve: .circular)
}
public var extendedEdges: UIEdgeInsets {
let left = self.bottomLeft.extendedInsets.width
let right = self.bottomRight.extendedInsets.width
return UIEdgeInsets(top: 0.0, left: left, bottom: 0.0, right: right)
}
public func withRemovedTails() -> ImageCorners {
return ImageCorners(topLeft: self.topLeft.withoutTail, topRight: self.topRight.withoutTail, bottomLeft: self.bottomLeft.withoutTail, bottomRight: self.bottomRight.withoutTail, curve: self.curve)
}
}
public func ==(lhs: ImageCorners, rhs: ImageCorners) -> Bool {
return lhs.topLeft == rhs.topLeft && lhs.topRight == rhs.topRight && lhs.bottomLeft == rhs.bottomLeft && lhs.bottomRight == rhs.bottomRight && lhs.curve == rhs.curve
}
public class ImageNode: ASDisplayNode {
private var disposable = MetaDisposable()
private let hasImage: ValuePromise<Bool>?
private var first = true
private let enableEmpty: Bool
public var enableAnimatedTransition: Bool
public var animateFirstTransition = true
private let _contentReady = Promise<Bool>()
private var didSetReady: Bool = false
public var contentReady: Signal<Bool, NoError> {
return self._contentReady.get()
}
public var ready: Signal<Bool, NoError> {
if let hasImage = self.hasImage {
return hasImage.get()
} else {
return .single(true)
}
}
public var contentUpdated: ((UIImage?) -> Void)?
public init(enableHasImage: Bool = false, enableEmpty: Bool = false, enableAnimatedTransition: Bool = false) {
if enableHasImage {
self.hasImage = ValuePromise(false, ignoreRepeated: true)
} else {
self.hasImage = nil
}
self.enableEmpty = enableEmpty
self.enableAnimatedTransition = enableAnimatedTransition
super.init()
}
deinit {
self.disposable.dispose()
}
public func setSignal(_ signal: Signal<UIImage?, NoError>) {
var reportedHasImage = false
var wasSynchronous = true
self.disposable.set((signal |> deliverOnMainQueue).start(next: {[weak self] next in
dispatcher.dispatch {
if let strongSelf = self {
var animate = strongSelf.enableAnimatedTransition
if strongSelf.first && next != nil {
strongSelf.first = false
animate = false
if strongSelf.isNodeLoaded && strongSelf.animateFirstTransition && !wasSynchronous {
strongSelf.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
}
}
if let image = next?.cgImage {
if animate, let previousContents = strongSelf.contents, !wasSynchronous {
strongSelf.contents = image
let tempLayer = CALayer()
tempLayer.contents = previousContents
tempLayer.frame = strongSelf.layer.bounds
strongSelf.layer.addSublayer(tempLayer)
tempLayer.opacity = 0.0
tempLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: true, completion: { [weak tempLayer] _ in
tempLayer?.removeFromSuperlayer()
})
//strongSelf.layer.animate(from: previousContents as! CGImage, to: image, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
} else {
strongSelf.contents = image
}
strongSelf.contentUpdated?(next)
} else if strongSelf.enableEmpty {
strongSelf.contents = nil
strongSelf.contentUpdated?(nil)
}
if !reportedHasImage {
if let hasImage = strongSelf.hasImage {
reportedHasImage = true
hasImage.set(true)
}
}
if !strongSelf.didSetReady {
strongSelf.didSetReady = true
strongSelf._contentReady.set(.single(true))
}
}
}
}))
wasSynchronous = false
}
public override func clearContents() {
super.clearContents()
self.contents = nil
self.disposable.set(nil)
self.contentUpdated?(nil)
}
public var image: UIImage? {
if let contents = self.contents {
return UIImage(cgImage: contents as! CGImage)
} else {
return nil
}
}
}
@@ -0,0 +1,409 @@
import Foundation
import UIKit
public struct ImmediateTextNodeLayoutInfo {
public let size: CGSize
public let truncated: Bool
public let numberOfLines: Int
public init(size: CGSize, truncated: Bool, numberOfLines: Int) {
self.size = size
self.truncated = truncated
self.numberOfLines = numberOfLines
}
}
public class ImmediateTextNode: TextNode {
public var attributedText: NSAttributedString?
public var textAlignment: NSTextAlignment = .natural
public var verticalAlignment: TextVerticalAlignment = .top
public var truncationType: CTLineTruncationType = .end
public var maximumNumberOfLines: Int = 1
public var lineSpacing: CGFloat = 0.0
public var insets: UIEdgeInsets = UIEdgeInsets()
public var textShadowColor: UIColor?
public var textShadowBlur: CGFloat?
public var textStroke: (UIColor, CGFloat)?
public var cutout: TextNodeCutout?
public var displaySpoilers = false
public var truncationMode: NSLineBreakMode {
get {
switch self.truncationType {
case .start:
return .byTruncatingHead
case .middle:
return .byTruncatingMiddle
case .end:
return .byTruncatingTail
@unknown default:
return .byTruncatingTail
}
} set(value) {
switch value {
case .byTruncatingHead:
self.truncationType = .start
case .byTruncatingMiddle:
self.truncationType = .middle
case .byTruncatingTail:
self.truncationType = .end
default:
self.truncationType = .end
}
}
}
private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer?
private var linkHighlightingNode: LinkHighlightingNode?
public var linkHighlightColor: UIColor?
public var trailingLineWidth: CGFloat?
var constrainedSize: CGSize?
public var highlightAttributeAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? {
didSet {
if self.isNodeLoaded {
self.updateInteractiveActions()
}
}
}
public var tapAttributeAction: (([NSAttributedString.Key: Any], Int) -> Void)?
public var longTapAttributeAction: (([NSAttributedString.Key: Any], Int) -> Void)?
public func makeCopy() -> TextNode {
let node = TextNode()
node.cachedLayout = self.cachedLayout
node.frame = self.frame
if let subnodes = self.subnodes {
for subnode in subnodes {
if let subnode = subnode as? ASImageNode {
let copySubnode = ASImageNode()
copySubnode.isLayerBacked = subnode.isLayerBacked
copySubnode.image = subnode.image
copySubnode.displaysAsynchronously = false
copySubnode.displayWithoutProcessing = true
copySubnode.frame = subnode.frame
copySubnode.alpha = subnode.alpha
node.addSubnode(copySubnode)
}
}
}
return node
}
public func updateLayout(_ constrainedSize: CGSize) -> CGSize {
self.constrainedSize = constrainedSize
let makeLayout = TextNode.asyncLayout(self)
let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, verticalAlignment: self.verticalAlignment, lineSpacing: self.lineSpacing, cutout: self.cutout, insets: self.insets, textShadowColor: self.textShadowColor, textShadowBlur: self.textShadowBlur, textStroke: self.textStroke, displaySpoilers: self.displaySpoilers))
let _ = apply()
if layout.numberOfLines > 1 {
self.trailingLineWidth = layout.trailingLineWidth
} else {
self.trailingLineWidth = nil
}
return layout.size
}
public func updateLayoutInfo(_ constrainedSize: CGSize) -> ImmediateTextNodeLayoutInfo {
self.constrainedSize = constrainedSize
let makeLayout = TextNode.asyncLayout(self)
let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, verticalAlignment: self.verticalAlignment, lineSpacing: self.lineSpacing, cutout: self.cutout, insets: self.insets, displaySpoilers: self.displaySpoilers))
let _ = apply()
return ImmediateTextNodeLayoutInfo(size: layout.size, truncated: layout.truncated, numberOfLines: layout.numberOfLines)
}
public func updateLayoutFullInfo(_ constrainedSize: CGSize) -> TextNodeLayout {
self.constrainedSize = constrainedSize
let makeLayout = TextNode.asyncLayout(self)
let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, verticalAlignment: self.verticalAlignment, lineSpacing: self.lineSpacing, cutout: self.cutout, insets: self.insets, displaySpoilers: self.displaySpoilers))
let _ = apply()
return layout
}
public func redrawIfPossible() {
if let constrainedSize = self.constrainedSize {
let _ = self.updateLayout(constrainedSize)
}
}
override open func didLoad() {
super.didLoad()
self.updateInteractiveActions()
}
private func updateInteractiveActions() {
if self.highlightAttributeAction != nil {
if self.tapRecognizer == nil {
let tapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapAction(_:)))
tapRecognizer.highlight = { [weak self] point in
if let strongSelf = self {
var rects: [CGRect]?
if let point = point {
if let (index, attributes) = strongSelf.attributesAtPoint(CGPoint(x: point.x, y: point.y)) {
if let selectedAttribute = strongSelf.highlightAttributeAction?(attributes) {
let initialRects = strongSelf.lineAndAttributeRects(name: selectedAttribute.rawValue, at: index)
if let initialRects = initialRects, case .center = strongSelf.textAlignment {
var mappedRects: [CGRect] = []
for i in 0 ..< initialRects.count {
let lineRect = initialRects[i].0
var itemRect = initialRects[i].1
itemRect.origin.x = floor((strongSelf.bounds.size.width - lineRect.width) / 2.0) + itemRect.origin.x
mappedRects.append(itemRect)
}
rects = mappedRects
} else {
rects = strongSelf.attributeRects(name: selectedAttribute.rawValue, at: index)
}
}
}
}
if let rects = rects {
let linkHighlightingNode: LinkHighlightingNode
if let current = strongSelf.linkHighlightingNode {
linkHighlightingNode = current
} else {
linkHighlightingNode = LinkHighlightingNode(color: strongSelf.linkHighlightColor ?? .clear)
strongSelf.linkHighlightingNode = linkHighlightingNode
strongSelf.addSubnode(linkHighlightingNode)
}
linkHighlightingNode.frame = strongSelf.bounds
linkHighlightingNode.updateRects(rects.map { $0.offsetBy(dx: 0.0, dy: 0.0) })
} else if let linkHighlightingNode = strongSelf.linkHighlightingNode {
strongSelf.linkHighlightingNode = nil
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
linkHighlightingNode?.removeFromSupernode()
})
}
}
}
self.view.addGestureRecognizer(tapRecognizer)
}
} else if let tapRecognizer = self.tapRecognizer {
self.tapRecognizer = nil
self.view.removeGestureRecognizer(tapRecognizer)
}
}
@objc private func tapAction(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
if let (index, attributes) = self.attributesAtPoint(CGPoint(x: location.x, y: location.y)) {
self.tapAttributeAction?(attributes, index)
}
case .longTap:
if let (index, attributes) = self.attributesAtPoint(CGPoint(x: location.x, y: location.y)) {
self.longTapAttributeAction?(attributes, index)
}
default:
break
}
}
default:
break
}
}
}
public class ASTextNode: ImmediateTextNode {
override public var attributedText: NSAttributedString? {
didSet {
self.setNeedsLayout()
}
}
override public init() {
super.init()
self.maximumNumberOfLines = 0
}
override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
return self.updateLayout(constrainedSize)
}
}
open class ImmediateTextView: TextView {
public var attributedText: NSAttributedString?
public var textAlignment: NSTextAlignment = .natural
public var verticalAlignment: TextVerticalAlignment = .top
public var truncationType: CTLineTruncationType = .end
public var maximumNumberOfLines: Int = 1
public var lineSpacing: CGFloat = 0.0
public var insets: UIEdgeInsets = UIEdgeInsets()
public var textShadowColor: UIColor?
public var textShadowBlur: CGFloat?
public var textStroke: (UIColor, CGFloat)?
public var cutout: TextNodeCutout?
public var displaySpoilers = false
public var truncationMode: NSLineBreakMode {
get {
switch self.truncationType {
case .start:
return .byTruncatingHead
case .middle:
return .byTruncatingMiddle
case .end:
return .byTruncatingTail
@unknown default:
return .byTruncatingTail
}
} set(value) {
switch value {
case .byTruncatingHead:
self.truncationType = .start
case .byTruncatingMiddle:
self.truncationType = .middle
case .byTruncatingTail:
self.truncationType = .end
default:
self.truncationType = .end
}
}
}
private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer?
private var linkHighlightingNode: LinkHighlightingNode?
public var linkHighlightColor: UIColor?
public var linkHighlightInset: UIEdgeInsets = .zero
public var trailingLineWidth: CGFloat?
var constrainedSize: CGSize?
public var highlightAttributeAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? {
didSet {
self.updateInteractiveActions()
}
}
public var tapAttributeAction: (([NSAttributedString.Key: Any], Int) -> Void)?
public var longTapAttributeAction: (([NSAttributedString.Key: Any], Int) -> Void)?
public func updateLayout(_ constrainedSize: CGSize) -> CGSize {
self.constrainedSize = constrainedSize
let makeLayout = TextView.asyncLayout(self)
let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, verticalAlignment: self.verticalAlignment, lineSpacing: self.lineSpacing, cutout: self.cutout, insets: self.insets, textShadowColor: self.textShadowColor, textShadowBlur: self.textShadowBlur, textStroke: self.textStroke, displaySpoilers: self.displaySpoilers))
let _ = apply()
if layout.numberOfLines > 1 {
self.trailingLineWidth = layout.trailingLineWidth
} else {
self.trailingLineWidth = nil
}
return layout.size
}
public func updateLayoutInfo(_ constrainedSize: CGSize) -> ImmediateTextNodeLayoutInfo {
self.constrainedSize = constrainedSize
let makeLayout = TextView.asyncLayout(self)
let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, verticalAlignment: self.verticalAlignment, lineSpacing: self.lineSpacing, cutout: self.cutout, insets: self.insets, displaySpoilers: self.displaySpoilers))
let _ = apply()
return ImmediateTextNodeLayoutInfo(size: layout.size, truncated: layout.truncated, numberOfLines: layout.numberOfLines)
}
public func updateLayoutFullInfo(_ constrainedSize: CGSize) -> TextNodeLayout {
self.constrainedSize = constrainedSize
let makeLayout = TextView.asyncLayout(self)
let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, verticalAlignment: self.verticalAlignment, lineSpacing: self.lineSpacing, cutout: self.cutout, insets: self.insets, displaySpoilers: self.displaySpoilers))
let _ = apply()
return layout
}
public func redrawIfPossible() {
if let constrainedSize = self.constrainedSize {
let _ = self.updateLayout(constrainedSize)
}
}
private func updateInteractiveActions() {
if self.highlightAttributeAction != nil {
if self.tapRecognizer == nil {
let tapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapAction(_:)))
tapRecognizer.highlight = { [weak self] point in
if let strongSelf = self {
var rects: [CGRect]?
if let point = point {
if let (index, attributes) = strongSelf.attributesAtPoint(CGPoint(x: point.x, y: point.y)) {
if let selectedAttribute = strongSelf.highlightAttributeAction?(attributes) {
let initialRects = strongSelf.lineAndAttributeRects(name: selectedAttribute.rawValue, at: index)
if let initialRects = initialRects, case .center = strongSelf.textAlignment {
var mappedRects: [CGRect] = []
for i in 0 ..< initialRects.count {
let lineRect = initialRects[i].0
var itemRect = initialRects[i].1
itemRect.origin.x = floor((strongSelf.bounds.size.width - lineRect.width) / 2.0) + itemRect.origin.x
mappedRects.append(itemRect)
}
rects = mappedRects
} else {
rects = strongSelf.attributeRects(name: selectedAttribute.rawValue, at: index)
}
}
}
}
if var rects, !rects.isEmpty {
let linkHighlightingNode: LinkHighlightingNode
if let current = strongSelf.linkHighlightingNode {
linkHighlightingNode = current
} else {
linkHighlightingNode = LinkHighlightingNode(color: strongSelf.linkHighlightColor ?? .clear)
strongSelf.linkHighlightingNode = linkHighlightingNode
strongSelf.addSubnode(linkHighlightingNode)
}
linkHighlightingNode.frame = strongSelf.bounds
rects[rects.count - 1] = rects[rects.count - 1].inset(by: strongSelf.linkHighlightInset)
linkHighlightingNode.updateRects(rects)
} else if let linkHighlightingNode = strongSelf.linkHighlightingNode {
strongSelf.linkHighlightingNode = nil
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
linkHighlightingNode?.removeFromSupernode()
})
}
}
}
self.addGestureRecognizer(tapRecognizer)
}
} else if let tapRecognizer = self.tapRecognizer {
self.tapRecognizer = nil
self.removeGestureRecognizer(tapRecognizer)
}
}
@objc private func tapAction(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
if let (index, attributes) = self.attributesAtPoint(CGPoint(x: location.x, y: location.y)) {
self.tapAttributeAction?(attributes, index)
}
case .longTap:
if let (index, attributes) = self.attributesAtPoint(CGPoint(x: location.x, y: location.y)) {
self.longTapAttributeAction?(attributes, index)
}
default:
break
}
}
default:
break
}
}
}
@@ -0,0 +1,216 @@
import Foundation
import UIKit
private enum HorizontalGestures {
case none
case some
case strict
}
private func hasHorizontalGestures(_ view: UIView, point: CGPoint?) -> HorizontalGestures {
if let disablesInteractiveTransitionGestureRecognizerNow = view.disablesInteractiveTransitionGestureRecognizerNow, disablesInteractiveTransitionGestureRecognizerNow() {
return .strict
}
if view.disablesInteractiveTransitionGestureRecognizer {
return .some
}
if let point = point, let test = view.interactiveTransitionGestureRecognizerTest, test(point) {
return .some
}
if let view = view as? ListViewBackingView {
let transform = view.transform
let angle: Double = Double(atan2f(Float(transform.b), Float(transform.a)))
let term1: Double = abs(angle - Double.pi / 2.0)
let term2: Double = abs(angle + Double.pi / 2.0)
let term3: Double = abs(angle - Double.pi * 3.0 / 2.0)
if term1 < 0.001 || term2 < 0.001 || term3 < 0.001 {
return .some
}
}
if let superview = view.superview {
return hasHorizontalGestures(superview, point: point != nil ? view.convert(point!, to: superview) : nil)
} else {
return .none
}
}
public struct InteractiveTransitionGestureRecognizerDirections: OptionSet {
public var rawValue: Int
public init(rawValue: Int) {
self.rawValue = rawValue
}
public static let leftEdge = InteractiveTransitionGestureRecognizerDirections(rawValue: 1 << 2)
public static let rightEdge = InteractiveTransitionGestureRecognizerDirections(rawValue: 1 << 3)
public static let leftCenter = InteractiveTransitionGestureRecognizerDirections(rawValue: 1 << 0)
public static let rightCenter = InteractiveTransitionGestureRecognizerDirections(rawValue: 1 << 1)
public static let down = InteractiveTransitionGestureRecognizerDirections(rawValue: 1 << 4)
public static let left: InteractiveTransitionGestureRecognizerDirections = [.leftEdge, .leftCenter]
public static let right: InteractiveTransitionGestureRecognizerDirections = [.rightEdge, .rightCenter]
}
public enum InteractiveTransitionGestureRecognizerEdgeWidth {
case constant(CGFloat)
case widthMultiplier(factor: CGFloat, min: CGFloat, max: CGFloat)
}
public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
private let edgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth
private let allowedDirections: (CGPoint) -> InteractiveTransitionGestureRecognizerDirections
private var validatedGesture = false
private var firstLocation: CGPoint = CGPoint()
private var currentAllowedDirections: InteractiveTransitionGestureRecognizerDirections = []
public init(target: Any?, action: Selector?, allowedDirections: @escaping (CGPoint) -> InteractiveTransitionGestureRecognizerDirections, edgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth = .constant(16.0)) {
self.allowedDirections = allowedDirections
self.edgeWidth = edgeWidth
super.init(target: target, action: action)
self.maximumNumberOfTouches = 1
self.delaysTouchesBegan = false
}
override public func reset() {
super.reset()
self.validatedGesture = false
self.currentAllowedDirections = []
}
public func cancel() {
self.state = .cancelled
}
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
let touch = touches.first!
let point = touch.location(in: self.view)
var allowedDirections = self.allowedDirections(point)
if allowedDirections.isEmpty {
self.state = .failed
return
}
super.touchesBegan(touches, with: event)
self.firstLocation = point
if let target = self.view?.hitTest(self.firstLocation, with: event) {
let horizontalGestures = hasHorizontalGestures(target, point: self.view?.convert(self.firstLocation, to: target))
switch horizontalGestures {
case .some, .strict:
if allowedDirections.contains(.down) {
} else {
if case .strict = horizontalGestures {
allowedDirections = []
} else if allowedDirections.contains(.leftEdge) || allowedDirections.contains(.rightEdge) {
allowedDirections.remove(.leftCenter)
allowedDirections.remove(.rightCenter)
}
}
case .none:
break
}
}
if allowedDirections.isEmpty {
self.state = .failed
} else {
self.currentAllowedDirections = allowedDirections
}
}
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
let location = touches.first!.location(in: self.view)
let translation = CGPoint(x: location.x - self.firstLocation.x, y: location.y - self.firstLocation.y)
let absTranslationX: CGFloat = abs(translation.x)
let absTranslationY: CGFloat = abs(translation.y)
let size = self.view?.bounds.size ?? CGSize()
//print("moved: \(CFAbsoluteTimeGetCurrent()) absTranslationX: \(absTranslationX) absTranslationY: \(absTranslationY)")
var fireBegan = false
if self.currentAllowedDirections.contains(.down) {
if !self.validatedGesture {
let totalMovement = sqrt(absTranslationX * absTranslationX + absTranslationY * absTranslationY)
if totalMovement > 10.0 {
// Force dominant direction after 10pt movement
if absTranslationY >= absTranslationX {
self.validatedGesture = true
} else {
self.state = .failed
}
} else if absTranslationX > 2.0 && absTranslationX > absTranslationY * 2.0 {
self.state = .failed
} else if absTranslationY > 2.0 && absTranslationX * 2.0 < absTranslationY {
self.validatedGesture = true
}
}
} else {
let edgeWidth: CGFloat
switch self.edgeWidth {
case let .constant(value):
edgeWidth = value
case let .widthMultiplier(factor, minValue, maxValue):
edgeWidth = max(minValue, min(size.width * factor, maxValue))
}
if !self.validatedGesture {
if self.firstLocation.x < edgeWidth && !self.currentAllowedDirections.contains(.rightEdge) {
self.state = .failed
return
}
if self.firstLocation.x > size.width - edgeWidth && !self.currentAllowedDirections.contains(.leftEdge) {
self.state = .failed
return
}
if self.currentAllowedDirections.contains(.rightEdge) && self.firstLocation.x < edgeWidth {
self.validatedGesture = true
} else if self.currentAllowedDirections.contains(.leftEdge) && self.firstLocation.x > size.width - edgeWidth {
self.validatedGesture = true
} else if !self.currentAllowedDirections.contains(.leftCenter) && translation.x < 0.0 {
self.state = .failed
} else if !self.currentAllowedDirections.contains(.rightCenter) && translation.x > 0.0 {
self.state = .failed
} else {
let totalMovement = sqrt(absTranslationX * absTranslationX + absTranslationY * absTranslationY)
if totalMovement > 10.0 {
// Force dominant direction after 10pt movement
if absTranslationX >= absTranslationY {
self.validatedGesture = true
fireBegan = true
} else {
self.state = .failed
}
} else if absTranslationY > 2.0 && absTranslationY > absTranslationX * 2.0 {
self.state = .failed
} else if absTranslationX > 2.0 && absTranslationY * 2.0 < absTranslationX {
self.validatedGesture = true
fireBegan = true
}
}
}
}
if self.validatedGesture {
super.touchesMoved(touches, with: event)
if fireBegan {
if self.state == .possible {
self.state = .began
}
}
}
}
}
@@ -0,0 +1,39 @@
import UIKit
public struct KeyShortcut: Hashable {
let title: String
let input: String
let modifiers: UIKeyModifierFlags
let action: () -> Void
public init(title: String = "", input: String = "", modifiers: UIKeyModifierFlags = [], action: @escaping () -> Void = {}) {
self.title = title
self.input = input
self.modifiers = modifiers
self.action = action
}
public func hash(into hasher: inout Hasher) {
hasher.combine(self.input)
hasher.combine(self.modifiers.rawValue)
}
public static func ==(lhs: KeyShortcut, rhs: KeyShortcut) -> Bool {
return lhs.hashValue == rhs.hashValue
}
}
extension KeyShortcut {
var uiKeyCommand: UIKeyCommand {
let command = UIKeyCommand(input: self.input, modifierFlags: self.modifiers, action: #selector(KeyShortcutsController.handleKeyCommand(_:)))
command.discoverabilityTitle = self.title
if #available(iOS 15.0, *), ["\t", UIKeyCommand.inputUpArrow, UIKeyCommand.inputDownArrow, UIKeyCommand.inputLeftArrow, UIKeyCommand.inputRightArrow].contains(command.input) && self.modifiers.isEmpty {
command.wantsPriorityOverSystemBehavior = true
}
return command
}
func isEqual(to command: UIKeyCommand) -> Bool {
return self.input == command.input && self.modifiers == command.modifierFlags
}
}
@@ -0,0 +1,80 @@
import UIKit
public protocol KeyShortcutResponder {
var keyShortcuts: [KeyShortcut] { get };
}
public class KeyShortcutsController: UIResponder {
private var effectiveShortcuts: [KeyShortcut]?
private var viewControllerEnumerator: (@escaping (ContainableController) -> Bool) -> Void
public static var isAvailable: Bool {
if #available(iOSApplicationExtension 8.0, iOS 8.0, *), UIDevice.current.userInterfaceIdiom == .pad {
return true
} else {
return false
}
}
public init(enumerator: @escaping (@escaping (ContainableController) -> Bool) -> Void) {
self.viewControllerEnumerator = enumerator
super.init()
}
public override var keyCommands: [UIKeyCommand]? {
var convertedCommands: [UIKeyCommand] = []
var shortcuts: [KeyShortcut] = []
self.viewControllerEnumerator({ viewController -> Bool in
guard let viewController = viewController as? KeyShortcutResponder else {
return true
}
shortcuts.removeAll(where: { viewController.keyShortcuts.contains($0) })
shortcuts.append(contentsOf: viewController.keyShortcuts)
return true
})
convertedCommands.append(contentsOf: shortcuts.map { $0.uiKeyCommand })
self.effectiveShortcuts = shortcuts
return convertedCommands
}
@objc func handleKeyCommand(_ command: UIKeyCommand) {
if let shortcut = findShortcut(for: command) {
shortcut.action()
}
}
private func findShortcut(for command: UIKeyCommand) -> KeyShortcut? {
if let shortcuts = self.effectiveShortcuts {
for shortcut in shortcuts {
if shortcut.isEqual(to: command) {
return shortcut
}
}
}
return nil
}
public override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if let keyCommand = sender as? UIKeyCommand, let _ = findShortcut(for: keyCommand) {
return true
} else {
return super.canPerformAction(action, withSender: sender)
}
}
public override func target(forAction action: Selector, withSender sender: Any?) -> Any? {
if let keyCommand = sender as? UIKeyCommand, let _ = findShortcut(for: keyCommand) {
return self
} else {
return super.target(forAction: action, withSender: sender)
}
}
public override var canBecomeFirstResponder: Bool {
return true
}
}
+9
View File
@@ -0,0 +1,9 @@
import Foundation
import UIKit
import UIKitRuntimeUtils
public enum Keyboard {
public static func applyAutocorrection(textView: UITextView) {
applyKeyboardAutocorrection(textView)
}
}
@@ -0,0 +1,177 @@
import Foundation
import UIKit
import AsyncDisplayKit
import UIKitRuntimeUtils
struct KeyboardSurface {
let host: UIView
}
public extension UIResponder {
private struct Static {
static weak var responder: UIResponder?
}
static func currentFirst() -> UIResponder? {
Static.responder = nil
UIApplication.shared.sendAction(#selector(UIResponder._trap), to: nil, from: nil, for: nil)
return Static.responder
}
@objc private func _trap() {
Static.responder = self
}
}
private func getFirstResponder(_ view: UIView) -> UIView? {
if view.isFirstResponder {
return view
} else {
for subview in view.subviews {
if let result = getFirstResponder(subview) {
return result
}
}
return nil
}
}
class KeyboardManager {
private let host: StatusBarHost
private weak var previousFirstResponderView: UIView?
private var interactiveInputOffset: CGFloat = 0.0
var surfaces: [KeyboardSurface] = [] {
didSet {
self.updateSurfaces(oldValue)
}
}
init(host: StatusBarHost) {
self.host = host
}
func getCurrentKeyboardHeight() -> CGFloat {
guard let keyboardView = self.host.keyboardView else {
return 0.0
}
if !isViewVisibleInHierarchy(keyboardView) {
return 0.0
}
return keyboardView.bounds.height
}
func updateInteractiveInputOffset(_ offset: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
guard let keyboardView = self.host.keyboardView else {
return
}
self.interactiveInputOffset = offset
let previousBounds = keyboardView.bounds
let updatedBounds = CGRect(origin: CGPoint(x: 0.0, y: -offset), size: previousBounds.size)
keyboardView.layer.bounds = updatedBounds
if transition.isAnimated {
transition.animateOffsetAdditive(layer: keyboardView.layer, offset: previousBounds.minY - updatedBounds.minY, completion: completion)
} else {
completion()
}
//transition.updateSublayerTransformOffset(layer: keyboardView.layer, offset: CGPoint(x: 0.0, y: offset))
}
private func updateSurfaces(_ previousSurfaces: [KeyboardSurface]) {
guard let keyboardWindow = self.host.keyboardWindow else {
return
}
var firstResponderView: UIView?
var firstResponderDisableAutomaticKeyboardHandling: UIResponderDisableAutomaticKeyboardHandling = []
for surface in self.surfaces {
if let view = getFirstResponder(surface.host) {
firstResponderView = surface.host
firstResponderDisableAutomaticKeyboardHandling = view.disableAutomaticKeyboardHandling
break
}
}
if let firstResponderView = firstResponderView {
let containerOrigin = firstResponderView.convert(CGPoint(), to: nil)
var filteredTranslation = containerOrigin.x
if firstResponderDisableAutomaticKeyboardHandling.contains(.forward) {
filteredTranslation = max(0.0, filteredTranslation)
}
if firstResponderDisableAutomaticKeyboardHandling.contains(.backward) {
filteredTranslation = min(0.0, filteredTranslation)
}
let horizontalTranslation = CATransform3DMakeTranslation(filteredTranslation, 0.0, 0.0)
let currentTransform = keyboardWindow.layer.sublayerTransform
if !CATransform3DEqualToTransform(horizontalTranslation, currentTransform) {
//print("set to \(CGPoint(x: containerOrigin.x, y: self.interactiveInputOffset))")
keyboardWindow.layer.sublayerTransform = horizontalTranslation
}
} else {
keyboardWindow.layer.sublayerTransform = CATransform3DIdentity
if let previousFirstResponderView = previousFirstResponderView {
if previousFirstResponderView.window == nil {
keyboardWindow.isHidden = true
keyboardWindow.layer.cancelAnimationsRecursive(key: "position")
keyboardWindow.layer.cancelAnimationsRecursive(key: "bounds")
keyboardWindow.isHidden = false
}
}
}
self.previousFirstResponderView = firstResponderView
}
}
private func endAnimations(view: UIView) {
view.layer.removeAllAnimations()
for subview in view.subviews {
endAnimations(view: subview)
}
}
public func viewTreeContainsFirstResponder(view: UIView) -> Bool {
if view.isFirstResponder {
return true
} else {
for subview in view.subviews {
if viewTreeContainsFirstResponder(view: subview) {
return true
}
}
return false
}
}
public final class KeyboardViewManager {
private let host: StatusBarHost
init(host: StatusBarHost) {
self.host = host
}
public func dismissEditingWithoutAnimation(view: UIView) {
if viewTreeContainsFirstResponder(view: view) {
view.endEditing(true)
if let keyboardWindow = self.host.keyboardWindow {
for view in keyboardWindow.subviews {
endAnimations(view: view)
}
}
}
}
public func update(leftEdge: CGFloat, transition: ContainedViewLayoutTransition) {
guard let keyboardWindow = self.host.keyboardWindow else {
return
}
let t = keyboardWindow.layer.sublayerTransform
let currentOffset = CGPoint(x: t.m41, y: t.m42)
transition.updateSublayerTransformOffset(layer: keyboardWindow.layer, offset: CGPoint(x: leftEdge, y: currentOffset.y), completion: { _ in
})
}
}
@@ -0,0 +1,10 @@
import Foundation
import UIKit
public func horizontalContainerFillingSizeForLayout(layout: ContainerViewLayout, sideInset: CGFloat) -> CGFloat {
if case .regular = layout.metrics.widthClass {
return min(layout.size.width, 414.0) - sideInset * 2.0
} else {
return min(layout.size.width, 428.0) - sideInset * 2.0
}
}
@@ -0,0 +1,148 @@
import Foundation
import UIKit
import AsyncDisplayKit
public enum LegacyPresentedControllerPresentation {
case custom
case modal
}
private func passControllerAppearanceAnimated(presentation: LegacyPresentedControllerPresentation) -> Bool {
switch presentation {
case .custom:
return false
case .modal:
return true
}
}
open class LegacyPresentedController: ViewController {
private let legacyController: UIViewController
private let presentation: LegacyPresentedControllerPresentation
private var controllerNode: LegacyPresentedControllerNode {
return self.displayNode as! LegacyPresentedControllerNode
}
private var loadedController = false
var controllerLoaded: (() -> Void)?
private let asPresentable = true
public init(legacyController: UIViewController, presentation: LegacyPresentedControllerPresentation) {
self.legacyController = legacyController
self.presentation = presentation
super.init(navigationBarPresentationData: nil)
/*legacyController.navigation_setDismiss { [weak self] in
self?.dismiss()
}*/
if !asPresentable {
self.addChild(legacyController)
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override open func loadDisplayNode() {
self.displayNode = LegacyPresentedControllerNode()
}
override open func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if self.ignoreAppearanceMethodInvocations() {
return
}
if !loadedController && !asPresentable {
loadedController = true
self.controllerNode.controllerView = self.legacyController.view
self.controllerNode.view.addSubview(self.legacyController.view)
self.legacyController.didMove(toParent: self)
if let controllerLoaded = self.controllerLoaded {
controllerLoaded()
}
}
if !asPresentable {
self.legacyController.viewWillAppear(animated && passControllerAppearanceAnimated(presentation: self.presentation))
}
}
override open func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if self.ignoreAppearanceMethodInvocations() {
return
}
if !asPresentable {
self.legacyController.viewWillDisappear(animated && passControllerAppearanceAnimated(presentation: self.presentation))
}
}
override open func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if self.ignoreAppearanceMethodInvocations() {
return
}
if asPresentable {
if !loadedController {
loadedController = true
//self.legacyController.modalPresentationStyle = .currentContext
self.present(self.legacyController, animated: false, completion: nil)
}
} else {
switch self.presentation {
case .modal:
self.controllerNode.animateModalIn()
self.legacyController.viewDidAppear(true)
case .custom:
self.legacyController.viewDidAppear(animated)
}
}
}
override open func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if !self.asPresentable {
self.legacyController.viewDidDisappear(animated && passControllerAppearanceAnimated(presentation: self.presentation))
}
}
override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
override open func dismiss(completion: (() -> Void)? = nil) {
switch self.presentation {
case .modal:
self.controllerNode.animateModalOut { [weak self] in
/*if let controller = self?.legacyController as? TGViewController {
controller.didDismiss()
} else if let controller = self?.legacyController as? TGNavigationController {
controller.didDismiss()
}*/
self?.presentingViewController?.dismiss(animated: false, completion: completion)
}
case .custom:
/*if let controller = self.legacyController as? TGViewController {
controller.didDismiss()
} else if let controller = self.legacyController as? TGNavigationController {
controller.didDismiss()
}*/
self.presentingViewController?.dismiss(animated: false, completion: completion)
}
}
}
@@ -0,0 +1,40 @@
import Foundation
import UIKit
import AsyncDisplayKit
final class LegacyPresentedControllerNode: ASDisplayNode {
private var containerLayout: ContainerViewLayout?
var controllerView: UIView? {
didSet {
if let controllerView = self.controllerView, let containerLayout = self.containerLayout {
controllerView.frame = CGRect(origin: CGPoint(), size: containerLayout.size)
}
}
}
override init() {
super.init()
self.setViewBlock({
return UITracingLayerView()
})
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.containerLayout = layout
if let controllerView = self.controllerView {
controllerView.frame = CGRect(origin: CGPoint(), size: layout.size)
}
}
func animateModalIn() {
self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
}
func animateModalOut(completion: @escaping () -> Void) {
self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in
completion()
})
}
}
@@ -0,0 +1,413 @@
import Foundation
import UIKit
import AsyncDisplayKit
private enum CornerType {
case topLeft
case topRight
case bottomLeft
case bottomRight
}
private func drawFullCorner(context: CGContext, color: UIColor, at point: CGPoint, type: CornerType, radius: CGFloat) {
if radius.isZero {
return
}
context.setFillColor(color.cgColor)
switch type {
case .topLeft:
context.clear(CGRect(origin: point, size: CGSize(width: radius, height: radius)))
context.fillEllipse(in: CGRect(origin: point, size: CGSize(width: radius * 2.0, height: radius * 2.0)))
case .topRight:
context.clear(CGRect(origin: CGPoint(x: point.x - radius, y: point.y), size: CGSize(width: radius, height: radius)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
case .bottomLeft:
context.clear(CGRect(origin: CGPoint(x: point.x, y: point.y - radius), size: CGSize(width: radius, height: radius)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
case .bottomRight:
context.clear(CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius, height: radius)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
}
}
private func drawConnectingCorner(context: CGContext, color: UIColor, at point: CGPoint, type: CornerType, radius: CGFloat) {
context.setFillColor(color.cgColor)
switch type {
case .topLeft:
context.fill(CGRect(origin: CGPoint(x: point.x - radius, y: point.y), size: CGSize(width: radius, height: radius)))
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
case .topRight:
context.fill(CGRect(origin: CGPoint(x: point.x, y: point.y), size: CGSize(width: radius, height: radius)))
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
case .bottomLeft:
context.fill(CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius, height: radius)))
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
case .bottomRight:
context.fill(CGRect(origin: CGPoint(x: point.x, y: point.y - radius), size: CGSize(width: radius, height: radius)))
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
}
}
public func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, outerRadius: CGFloat, innerRadius: CGFloat, stroke: Bool = false, strokeWidth: CGFloat = 2.0, useModernPathCalculation: Bool) -> (CGPoint, UIImage?) {
if rects.isEmpty {
return (CGPoint(), nil)
}
var topLeft = rects[0].origin
var bottomRight = CGPoint(x: rects[0].maxX, y: rects[0].maxY)
for i in 1 ..< rects.count {
topLeft.x = min(topLeft.x, rects[i].origin.x)
topLeft.y = min(topLeft.y, rects[i].origin.y)
bottomRight.x = max(bottomRight.x, rects[i].maxX)
bottomRight.y = max(bottomRight.y, rects[i].maxY)
}
var drawingInset = inset
if stroke {
drawingInset += 2.0
}
topLeft.x -= drawingInset
topLeft.y -= drawingInset
bottomRight.x += drawingInset * 2.0
bottomRight.y += drawingInset * 2.0
return (topLeft, generateImage(CGSize(width: bottomRight.x - topLeft.x, height: bottomRight.y - topLeft.y), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(color.cgColor)
context.setBlendMode(.copy)
if useModernPathCalculation {
if rects.count == 1 {
let path = UIBezierPath(roundedRect: rects[0].offsetBy(dx: -topLeft.x, dy: -topLeft.y), cornerRadius: outerRadius).cgPath
context.addPath(path)
if stroke {
context.setStrokeColor(color.cgColor)
context.setLineWidth(strokeWidth)
context.strokePath()
} else {
context.fillPath()
}
return
}
var combinedRects: [[CGRect]] = []
var currentRects: [CGRect] = []
for rect in rects {
if rect.width.isZero {
if !currentRects.isEmpty {
combinedRects.append(currentRects)
}
currentRects.removeAll()
} else {
currentRects.append(rect)
}
}
if !currentRects.isEmpty {
combinedRects.append(currentRects)
}
for rects in combinedRects {
var rects = rects.map { $0.insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y) }
let minRadius: CGFloat = 2.0
for _ in 0 ..< rects.count * rects.count {
var hadChanges = false
for i in 0 ..< rects.count - 1 {
if rects[i].maxY > rects[i + 1].minY {
let midY = floor((rects[i].maxY + rects[i + 1].minY) * 0.5)
rects[i].size.height = midY - rects[i].minY
rects[i + 1].origin.y = midY
rects[i + 1].size.height = rects[i + 1].maxY - midY
hadChanges = true
}
if rects[i].maxY >= rects[i + 1].minY && rects[i].insetBy(dx: 0.0, dy: 1.0).intersects(rects[i + 1]) {
if abs(rects[i].minX - rects[i + 1].minX) < minRadius {
let commonMinX = min(rects[i].origin.x, rects[i + 1].origin.x)
if rects[i].origin.x != commonMinX {
rects[i].origin.x = commonMinX
hadChanges = true
}
if rects[i + 1].origin.x != commonMinX {
rects[i + 1].origin.x = commonMinX
hadChanges = true
}
}
if abs(rects[i].maxX - rects[i + 1].maxX) < minRadius {
let commonMaxX = max(rects[i].maxX, rects[i + 1].maxX)
if rects[i].maxX != commonMaxX {
rects[i].size.width = commonMaxX - rects[i].minX
hadChanges = true
}
if rects[i + 1].maxX != commonMaxX {
rects[i + 1].size.width = commonMaxX - rects[i + 1].minX
hadChanges = true
}
}
}
}
if !hadChanges {
break
}
}
context.move(to: CGPoint(x: rects[0].midX, y: rects[0].minY))
context.addLine(to: CGPoint(x: rects[0].maxX - outerRadius, y: rects[0].minY))
context.addArc(tangent1End: rects[0].topRight, tangent2End: CGPoint(x: rects[0].maxX, y: rects[0].minY + outerRadius), radius: outerRadius)
context.addLine(to: CGPoint(x: rects[0].maxX, y: rects[0].midY))
for i in 0 ..< rects.count - 1 {
let rect = rects[i]
let next = rects[i + 1]
if rect.maxX == next.maxX {
context.addLine(to: CGPoint(x: next.maxX, y: next.midY))
} else {
let nextRadius = min(outerRadius, floor(abs(rect.maxX - next.maxX) * 0.5))
context.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - nextRadius))
if next.maxX > rect.maxX {
context.addArc(tangent1End: CGPoint(x: rect.maxX, y: rect.maxY), tangent2End: CGPoint(x: rect.maxX + nextRadius, y: rect.maxY), radius: nextRadius)
context.addLine(to: CGPoint(x: next.maxX - nextRadius, y: next.minY))
} else {
context.addArc(tangent1End: CGPoint(x: rect.maxX, y: rect.maxY), tangent2End: CGPoint(x: rect.maxX - nextRadius, y: rect.maxY), radius: nextRadius)
context.addLine(to: CGPoint(x: next.maxX + nextRadius, y: next.minY))
}
context.addArc(tangent1End: next.topRight, tangent2End: CGPoint(x: next.maxX, y: next.minY + nextRadius), radius: nextRadius)
context.addLine(to: CGPoint(x: next.maxX, y: next.midY))
}
}
let last = rects[rects.count - 1]
context.addLine(to: CGPoint(x: last.maxX, y: last.maxY - outerRadius))
context.addArc(tangent1End: last.bottomRight, tangent2End: CGPoint(x: last.maxX - outerRadius, y: last.maxY), radius: outerRadius)
context.addLine(to: CGPoint(x: last.minX + outerRadius, y: last.maxY))
context.addArc(tangent1End: last.bottomLeft, tangent2End: CGPoint(x: last.minX, y: last.maxY - outerRadius), radius: outerRadius)
for i in (1 ..< rects.count).reversed() {
let rect = rects[i]
let prev = rects[i - 1]
if rect.minX == prev.minX {
context.addLine(to: CGPoint(x: prev.minX, y: prev.midY))
} else {
let prevRadius = min(outerRadius, floor(abs(rect.minX - prev.minX) * 0.5))
context.addLine(to: CGPoint(x: rect.minX, y: rect.minY + prevRadius))
if rect.minX < prev.minX {
context.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.minY), tangent2End: CGPoint(x: rect.minX + prevRadius, y: rect.minY), radius: prevRadius)
context.addLine(to: CGPoint(x: prev.minX - prevRadius, y: prev.maxY))
} else {
context.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.minY), tangent2End: CGPoint(x: rect.minX - prevRadius, y: rect.minY), radius: prevRadius)
context.addLine(to: CGPoint(x: prev.minX + prevRadius, y: prev.maxY))
}
context.addArc(tangent1End: prev.bottomLeft, tangent2End: CGPoint(x: prev.minX, y: prev.maxY - prevRadius), radius: prevRadius)
context.addLine(to: CGPoint(x: prev.minX, y: prev.midY))
}
}
context.addLine(to: CGPoint(x: rects[0].minX, y: rects[0].minY + outerRadius))
context.addArc(tangent1End: rects[0].topLeft, tangent2End: CGPoint(x: rects[0].minX + outerRadius, y: rects[0].minY), radius: outerRadius)
context.addLine(to: CGPoint(x: rects[0].midX, y: rects[0].minY))
if stroke {
context.setStrokeColor(color.cgColor)
context.setLineWidth(strokeWidth)
context.strokePath()
} else {
context.fillPath()
}
}
return
}
for i in 0 ..< rects.count {
let rect = rects[i].insetBy(dx: -inset, dy: -inset)
context.fill(rect.offsetBy(dx: -topLeft.x, dy: -topLeft.y))
}
for i in 0 ..< rects.count {
let rect = rects[i].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y)
var previous: CGRect?
if i != 0 {
previous = rects[i - 1].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y)
}
var next: CGRect?
if i != rects.count - 1 {
next = rects[i + 1].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y)
}
if let previous = previous {
if previous.contains(rect.topLeft) {
if abs(rect.topLeft.x - previous.minX) >= innerRadius {
var radius = innerRadius
if let next = next {
radius = min(radius, floor((next.minY - previous.maxY) / 2.0))
}
drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.topLeft.x, y: previous.maxY), type: .topLeft, radius: radius)
}
} else {
drawFullCorner(context: context, color: color, at: rect.topLeft, type: .topLeft, radius: outerRadius)
}
if previous.contains(rect.topRight.offsetBy(dx: -1.0, dy: 0.0)) {
if abs(rect.topRight.x - previous.maxX) >= innerRadius {
var radius = innerRadius
if let next = next {
radius = min(radius, floor((next.minY - previous.maxY) / 2.0))
}
drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.topRight.x, y: previous.maxY), type: .topRight, radius: radius)
}
} else {
drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: outerRadius)
}
} else {
drawFullCorner(context: context, color: color, at: rect.topLeft, type: .topLeft, radius: outerRadius)
drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: outerRadius)
}
if let next = next {
if next.contains(rect.bottomLeft) {
if abs(rect.bottomRight.x - next.maxX) >= innerRadius {
var radius = innerRadius
if let previous = previous {
radius = min(radius, floor((next.minY - previous.maxY) / 2.0))
}
drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.bottomLeft.x, y: next.minY), type: .bottomLeft, radius: radius)
}
} else {
drawFullCorner(context: context, color: color, at: rect.bottomLeft, type: .bottomLeft, radius: outerRadius)
}
if next.contains(rect.bottomRight.offsetBy(dx: -1.0, dy: 0.0)) {
if abs(rect.bottomRight.x - next.maxX) >= innerRadius {
var radius = innerRadius
if let previous = previous {
radius = min(radius, floor((next.minY - previous.maxY) / 2.0))
}
drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.bottomRight.x, y: next.minY), type: .bottomRight, radius: radius)
}
} else {
drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: outerRadius)
}
} else {
drawFullCorner(context: context, color: color, at: rect.bottomLeft, type: .bottomLeft, radius: outerRadius)
drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: outerRadius)
}
}
}))
}
public final class LinkHighlightingNode: ASDisplayNode {
public private(set) var rects: [CGRect] = []
public let imageNode: ASImageNode
public var innerRadius: CGFloat = 4.0
public var outerRadius: CGFloat = 4.0
public var inset: CGFloat = 2.0
public var useModernPathCalculation: Bool = false
public var borderOnly: Bool = false
public var strokeWidth: CGFloat = 1.0
private var _color: UIColor
public var color: UIColor {
get {
return _color
} set(value) {
self._color = value
if !self.rects.isEmpty {
self.updateImage()
}
}
}
public init(color: UIColor) {
self._color = color
self.imageNode = ASImageNode()
self.imageNode.isUserInteractionEnabled = false
self.imageNode.displaysAsynchronously = false
super.init()
self.addSubnode(self.imageNode)
}
public func updateRects(_ rects: [CGRect], color: UIColor? = nil) {
var updated = false
if self.rects != rects {
updated = true
self.rects = rects
}
if let color, !color.isEqual(self.color) {
updated = true
self.color = color
}
if updated {
self.updateImage()
}
}
private func updateImage() {
if self.rects.isEmpty {
self.imageNode.image = nil
}
let (offset, image) = generateRectsImage(color: self.color, rects: self.rects, inset: self.inset, outerRadius: self.outerRadius, innerRadius: self.innerRadius, stroke: self.borderOnly, strokeWidth: self.strokeWidth, useModernPathCalculation: self.useModernPathCalculation)
if let image = image {
self.imageNode.image = image
self.imageNode.frame = CGRect(origin: offset, size: image.size)
}
}
public static func generateImage(color: UIColor, inset: CGFloat, innerRadius: CGFloat, outerRadius: CGFloat, rects: [CGRect], useModernPathCalculation: Bool) -> (CGPoint, UIImage)? {
if rects.isEmpty {
return nil
}
let (offset, image) = generateRectsImage(color: color, rects: rects, inset: inset, outerRadius: outerRadius, innerRadius: innerRadius, useModernPathCalculation: useModernPathCalculation)
if let image = image {
return (offset, image)
} else {
return nil
}
}
public func asyncLayout() -> (UIColor, [CGRect], CGFloat, CGFloat, CGFloat) -> () -> Void {
let currentRects = self.rects
let currentColor = self._color
let currentInnerRadius = self.innerRadius
let currentOuterRadius = self.outerRadius
let currentInset = self.inset
let useModernPathCalculation = self.useModernPathCalculation
return { [weak self] color, rects, innerRadius, outerRadius, inset in
var updatedImage: (CGPoint, UIImage?)?
if currentRects != rects || !currentColor.isEqual(color) || currentInnerRadius != innerRadius || currentOuterRadius != outerRadius || currentInset != inset {
updatedImage = generateRectsImage(color: color, rects: rects, inset: inset, outerRadius: outerRadius, innerRadius: innerRadius, useModernPathCalculation: useModernPathCalculation)
}
return {
if let strongSelf = self {
strongSelf._color = color
strongSelf.rects = rects
strongSelf.innerRadius = innerRadius
strongSelf.outerRadius = outerRadius
strongSelf.inset = inset
if let (offset, maybeImage) = updatedImage, let image = maybeImage {
strongSelf.imageNode.image = image
strongSelf.imageNode.frame = CGRect(origin: offset, size: image.size)
}
}
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,6 @@
import Foundation
public protocol ListViewAccessoryItem {
func isEqualToItem(_ other: ListViewAccessoryItem) -> Bool
func node(synchronous: Bool) -> ListViewAccessoryItemNode
}
@@ -0,0 +1,50 @@
import Foundation
import UIKit
import AsyncDisplayKit
open class ListViewAccessoryItemNode: ASDisplayNode {
var transitionOffset: CGPoint = CGPoint() {
didSet {
self.bounds = CGRect(origin: self.transitionOffset, size: self.bounds.size)
}
}
private var transitionOffsetAnimation: ListViewAnimation?
final func animateTransitionOffset(_ from: CGPoint, beginAt: Double, duration: Double, curve: @escaping (CGFloat) -> CGFloat) {
self.transitionOffset = from
self.transitionOffsetAnimation = ListViewAnimation(from: from, to: CGPoint(), duration: duration, curve: curve, beginAt: beginAt, update: { [weak self] _, currentValue in
if let strongSelf = self {
strongSelf.transitionOffset = currentValue
}
})
}
final func removeAllAnimations() {
self.transitionOffsetAnimation = nil
self.transitionOffset = CGPoint()
}
final func animate(_ timestamp: Double) -> Bool {
if let animation = self.transitionOffsetAnimation {
animation.applyAt(timestamp)
if animation.completeAt(timestamp) {
self.transitionOffsetAnimation = nil
} else {
return true
}
}
return false
}
override open func layout() {
super.layout()
self.updateLayout(size: self.bounds.size, leftInset: 0.0, rightInset: 0.0)
}
open func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) {
}
}
@@ -0,0 +1,229 @@
import Foundation
import UIKit
import UIKitRuntimeUtils
public protocol Interpolatable {
static func interpolator() -> (Interpolatable, Interpolatable, CGFloat) -> (Interpolatable)
}
private func floorToPixels(_ value: CGFloat) -> CGFloat {
return value
}
private func floorToPixels(_ value: CGPoint) -> CGPoint {
return CGPoint(x: floorToPixels(value.x), y: floorToPixels(value.y))
}
private func floorToPixels(_ value: CGSize) -> CGSize {
return CGSize(width: floorToPixels(value.width), height: floorToPixels(value.height))
}
private func floorToPixels(_ value: CGRect) -> CGRect {
return CGRect(origin: floorToPixels(value.origin), size: floorToPixels(value.size))
}
private func floorToPixels(_ value: UIEdgeInsets) -> UIEdgeInsets {
return UIEdgeInsets(top: floorToPixels(value.top), left: floorToPixels(value.left), bottom: floorToPixels(value.bottom), right: floorToPixels(value.right))
}
extension CGFloat: Interpolatable {
public static func interpolator() -> (Interpolatable, Interpolatable, CGFloat) -> Interpolatable {
return { from, to, t -> Interpolatable in
let fromValue: CGFloat = from as! CGFloat
let toValue: CGFloat = to as! CGFloat
let invT: CGFloat = 1.0 - t
let term: CGFloat = toValue * t + fromValue * invT
return floorToPixels(term)
}
}
static func interpolate(from fromValue: CGFloat, to toValue: CGFloat, at t: CGFloat) -> CGFloat {
let invT: CGFloat = 1.0 - t
let term: CGFloat = toValue * t + fromValue * invT
return term
}
}
extension UIEdgeInsets: Interpolatable {
public static func interpolator() -> (Interpolatable, Interpolatable, CGFloat) -> Interpolatable {
return { from, to, t -> Interpolatable in
let fromValue = from as! UIEdgeInsets
let toValue = to as! UIEdgeInsets
return floorToPixels(UIEdgeInsets(top: toValue.top * t + fromValue.top * (1.0 - t), left: toValue.left * t + fromValue.left * (1.0 - t), bottom: toValue.bottom * t + fromValue.bottom * (1.0 - t), right: toValue.right * t + fromValue.right * (1.0 - t)))
}
}
}
extension CGRect: Interpolatable {
public static func interpolator() -> (Interpolatable, Interpolatable, CGFloat) -> Interpolatable {
return { from, to, t -> Interpolatable in
let fromValue = from as! CGRect
let toValue = to as! CGRect
return floorToPixels(CGRect(x: toValue.origin.x * t + fromValue.origin.x * (1.0 - t), y: toValue.origin.y * t + fromValue.origin.y * (1.0 - t), width: toValue.size.width * t + fromValue.size.width * (1.0 - t), height: toValue.size.height * t + fromValue.size.height * (1.0 - t)))
}
}
static func interpolate(from fromValue: CGRect, to toValue: CGRect, at t: CGFloat) -> CGRect {
return CGRect(origin: CGPoint.interpolate(from: fromValue.origin, to: toValue.origin, at: t), size: CGSize.interpolate(from: fromValue.size, to: toValue.size, at: t))
}
}
extension CGPoint: Interpolatable {
public static func interpolator() -> (Interpolatable, Interpolatable, CGFloat) -> Interpolatable {
return { from, to, t -> Interpolatable in
let fromValue = from as! CGPoint
let toValue = to as! CGPoint
return floorToPixels(CGPoint(x: toValue.x * t + fromValue.x * (1.0 - t), y: toValue.y * t + fromValue.y * (1.0 - t)))
}
}
static func interpolate(from fromValue: CGPoint, to toValue: CGPoint, at t: CGFloat) -> CGPoint {
return CGPoint(x: toValue.x * t + fromValue.x * (1.0 - t), y: toValue.y * t + fromValue.y * (1.0 - t))
}
}
extension CGSize {
static func interpolate(from fromValue: CGSize, to toValue: CGSize, at t: CGFloat) -> CGSize {
return CGSize(width: toValue.width * t + fromValue.width * (1.0 - t), height: toValue.height * t + fromValue.height * (1.0 - t))
}
}
private let springAnimationIn: CABasicAnimation = {
let animation = makeSpringAnimation("", duration: 0.5)
return animation
}()
let springAnimationSolver: (CGFloat) -> CGFloat = { () -> (CGFloat) -> CGFloat in
if #available(iOS 9.0, *) {
return { t in
return springAnimationValueAt(springAnimationIn, t)
}
} else {
return { t in
return bezierPoint(0.23, 1.0, 0.32, 1.0, t)
}
}
}()
public let listViewAnimationCurveSystem: (CGFloat) -> CGFloat = { t in
return springAnimationSolver(t)
}
public let listViewAnimationCurveLinear: (CGFloat) -> CGFloat = { t in
return t
}
public let listViewAnimationCurveEaseInOut: (CGFloat) -> CGFloat = { t in
return bezierPoint(0.42, 0.0, 0.58, 1.0, t)
}
#if os(iOS)
public func listViewAnimationCurveFromAnimationOptions(animationOptions: UIView.AnimationOptions) -> (CGFloat) -> CGFloat {
if animationOptions.rawValue == UInt(7 << 16) {
return listViewAnimationCurveSystem
} else {
return listViewAnimationCurveLinear
}
}
#endif
public final class ListViewAnimation {
let from: Interpolatable
public let to: Interpolatable
let duration: Double
let startTime: Double
let invertOffsetDirection: Bool
private let curve: (CGFloat) -> CGFloat
private let interpolator: (Interpolatable, Interpolatable, CGFloat) -> Interpolatable
private let update: (CGFloat, Interpolatable) -> Void
private let completed: (Bool) -> Void
public init<T: Interpolatable>(from: T, to: T, duration: Double, invertOffsetDirection: Bool = false, curve: @escaping (CGFloat) -> CGFloat, beginAt: Double, update: @escaping (CGFloat, T) -> Void, completed: @escaping (Bool) -> Void = { _ in }) {
self.from = from
self.to = to
self.duration = duration
self.invertOffsetDirection = invertOffsetDirection
self.curve = curve
self.startTime = beginAt
self.interpolator = T.interpolator()
self.update = { progress, value in
update(progress, value as! T)
}
self.completed = completed
}
init<T: Interpolatable>(copying: ListViewAnimation, update: @escaping (CGFloat, T) -> Void, completed: @escaping (Bool) -> Void = { _ in }) {
self.from = copying.from
self.to = copying.to
self.duration = copying.duration
self.curve = copying.curve
self.startTime = copying.startTime
self.interpolator = copying.interpolator
self.invertOffsetDirection = copying.invertOffsetDirection
self.update = { progress, value in
update(progress, value as! T)
}
self.completed = completed
}
public func completeAt(_ timestamp: Double) -> Bool {
if timestamp >= self.startTime + self.duration {
self.completed(true)
return true
} else {
return false
}
}
public func cancel() {
self.completed(false)
}
private func valueAt(_ t: CGFloat) -> Interpolatable {
if t <= 0.0 {
return self.from
} else if t >= 1.0 {
return self.to
} else {
return self.interpolator(self.from, self.to, t)
}
}
public func applyAt(_ timestamp: Double) {
var t = CGFloat((timestamp - self.startTime) / self.duration)
let ct: CGFloat
if t <= 0.0 + CGFloat.ulpOfOne {
t = 0.0
ct = 0.0
} else if t >= 1.0 - CGFloat.ulpOfOne {
t = 1.0
ct = 1.0
} else {
ct = self.curve(t)
}
self.update(ct, self.valueAt(ct))
}
}
public func listViewAnimationDurationAndCurve(transition: ContainedViewLayoutTransition) -> (Double, ListViewAnimationCurve) {
switch transition {
case .immediate:
return (0.0, .Default(duration: 0.0))
case let .animated(animationDuration, animationCurve):
switch animationCurve {
case .linear:
return (animationDuration, .Default(duration: animationDuration))
case .easeInOut:
return (animationDuration, .Default(duration: animationDuration))
case .spring, .customSpring:
return (animationDuration, .Spring(duration: animationDuration))
case let .custom(c1x, c1y, c2x, c2y):
return (animationDuration, .Custom(duration: animationDuration, c1x, c1y, c2x, c2y))
}
}
}
public func scrollingRubberBandingOffset(offset: CGFloat, bandingStart: CGFloat, range: CGFloat, coefficient: CGFloat = 0.4) -> CGFloat {
let bandedOffset = offset - bandingStart
return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range
}
@@ -0,0 +1,9 @@
import Foundation
import UIKit
import AsyncDisplayKit
open class ListViewFloatingHeaderNode: ASDisplayNode {
open func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
return 0.0
}
}
@@ -0,0 +1,915 @@
import Foundation
import UIKit
import SwiftSignalKit
public enum ListViewCenterScrollPositionOverflow: Equatable {
case top
case bottom
case custom((ListViewItemNode) -> CGFloat)
public static func ==(lhs: ListViewCenterScrollPositionOverflow, rhs: ListViewCenterScrollPositionOverflow) -> Bool {
switch lhs {
case .top:
if case .top = rhs {
return true
} else {
return false
}
case .bottom:
if case .bottom = rhs {
return true
} else {
return false
}
case .custom:
if case .custom = rhs {
return true
} else {
return false
}
}
}
}
public enum ListViewScrollPosition: Equatable {
case top(CGFloat)
case bottom(CGFloat)
case center(ListViewCenterScrollPositionOverflow)
case visible
}
public enum ListViewScrollToItemDirectionHint {
case Up
case Down
}
public enum ListViewAnimationCurve {
case Spring(duration: Double)
case Default(duration: Double?)
case Custom(duration: Double, Float, Float, Float, Float)
}
public struct ListViewScrollToItem {
public let index: Int
public let position: ListViewScrollPosition
public let animated: Bool
public let curve: ListViewAnimationCurve
public let directionHint: ListViewScrollToItemDirectionHint
public let displayLink: Bool
public init(index: Int, position: ListViewScrollPosition, animated: Bool, curve: ListViewAnimationCurve, directionHint: ListViewScrollToItemDirectionHint, displayLink: Bool = false) {
self.index = index
self.position = position
self.animated = animated
self.curve = curve
self.directionHint = directionHint
self.displayLink = displayLink
}
}
public enum ListViewItemOperationDirectionHint {
case Up
case Down
}
public struct ListViewDeleteItem {
public let index: Int
public let directionHint: ListViewItemOperationDirectionHint?
public init(index: Int, directionHint: ListViewItemOperationDirectionHint?) {
self.index = index
self.directionHint = directionHint
}
}
public struct ListViewInsertItem {
public let index: Int
public let previousIndex: Int?
public let item: ListViewItem
public let directionHint: ListViewItemOperationDirectionHint?
public let forceAnimateInsertion: Bool
public init(index: Int, previousIndex: Int?, item: ListViewItem, directionHint: ListViewItemOperationDirectionHint?, forceAnimateInsertion: Bool = false) {
self.index = index
self.previousIndex = previousIndex
self.item = item
self.directionHint = directionHint
self.forceAnimateInsertion = forceAnimateInsertion
}
}
public struct ListViewUpdateItem {
public let index: Int
public let previousIndex: Int
public let item: ListViewItem
public let directionHint: ListViewItemOperationDirectionHint?
public init(index: Int, previousIndex: Int, item: ListViewItem, directionHint: ListViewItemOperationDirectionHint?) {
self.index = index
self.previousIndex = previousIndex
self.item = item
self.directionHint = directionHint
}
}
public struct ListViewDeleteAndInsertOptions: OptionSet {
public let rawValue: Int
public init(rawValue: Int) {
self.rawValue = rawValue
}
public static let AnimateInsertion = ListViewDeleteAndInsertOptions(rawValue: 1)
public static let AnimateAlpha = ListViewDeleteAndInsertOptions(rawValue: 2)
public static let LowLatency = ListViewDeleteAndInsertOptions(rawValue: 4)
public static let Synchronous = ListViewDeleteAndInsertOptions(rawValue: 8)
public static let RequestItemInsertionAnimations = ListViewDeleteAndInsertOptions(rawValue: 16)
public static let AnimateTopItemPosition = ListViewDeleteAndInsertOptions(rawValue: 32)
public static let PreferSynchronousDrawing = ListViewDeleteAndInsertOptions(rawValue: 64)
public static let PreferSynchronousResourceLoading = ListViewDeleteAndInsertOptions(rawValue: 128)
public static let AnimateCrossfade = ListViewDeleteAndInsertOptions(rawValue: 256)
public static let ForceUpdate = ListViewDeleteAndInsertOptions(rawValue: 512)
public static let AnimateFullTransition = ListViewDeleteAndInsertOptions(rawValue: 1024)
public static let InvertOffsetDirection = ListViewDeleteAndInsertOptions(rawValue: 2048)
}
public struct ListViewUpdateSizeAndInsets {
public var size: CGSize
public var insets: UIEdgeInsets
public var headerInsets: UIEdgeInsets?
public var scrollIndicatorInsets: UIEdgeInsets?
public var itemOffsetInsets: UIEdgeInsets?
public var duration: Double
public var curve: ListViewAnimationCurve
public var ensureTopInsetForOverlayHighlightedItems: CGFloat?
public var customAnimationTransition: ControlledTransition?
public init(size: CGSize, insets: UIEdgeInsets, headerInsets: UIEdgeInsets? = nil, scrollIndicatorInsets: UIEdgeInsets? = nil, itemOffsetInsets: UIEdgeInsets? = nil, duration: Double, curve: ListViewAnimationCurve, ensureTopInsetForOverlayHighlightedItems: CGFloat? = nil, customAnimationTransition: ControlledTransition? = nil) {
self.size = size
self.insets = insets
self.headerInsets = headerInsets
self.scrollIndicatorInsets = scrollIndicatorInsets
self.itemOffsetInsets = itemOffsetInsets
self.duration = duration
self.curve = curve
self.ensureTopInsetForOverlayHighlightedItems = ensureTopInsetForOverlayHighlightedItems
self.customAnimationTransition = customAnimationTransition
}
}
public struct ListViewItemRange: Equatable {
public let firstIndex: Int
public let lastIndex: Int
}
public struct ListViewVisibleItemRange: Equatable {
public let firstIndex: Int
public let firstIndexFullyVisible: Bool
public let lastIndex: Int
}
public struct ListViewDisplayedItemRange: Equatable {
public let loadedRange: ListViewItemRange?
public let visibleRange: ListViewVisibleItemRange?
}
struct IndexRange {
let first: Int
let last: Int
func contains(_ index: Int) -> Bool {
return index >= first && index <= last
}
var empty: Bool {
return first > last
}
}
struct OffsetRanges {
var offsets: [(IndexRange, CGFloat)] = []
mutating func append(_ other: OffsetRanges) {
self.offsets.append(contentsOf: other.offsets)
}
mutating func offset(_ indexRange: IndexRange, offset: CGFloat) {
self.offsets.append((indexRange, offset))
}
func offsetForIndex(_ index: Int) -> CGFloat {
var result: CGFloat = 0.0
for offset in self.offsets {
if offset.0.contains(index) {
result += offset.1
}
}
return result
}
}
func binarySearch(_ inputArr: [Int], searchItem: Int) -> Int? {
var lowerIndex = 0;
var upperIndex = inputArr.count - 1
if lowerIndex > upperIndex {
return nil
}
while (true) {
let currentIndex = (lowerIndex + upperIndex) / 2
if (inputArr[currentIndex] == searchItem) {
return currentIndex
} else if (lowerIndex > upperIndex) {
return nil
} else {
if (inputArr[currentIndex] > searchItem) {
upperIndex = currentIndex - 1
} else {
lowerIndex = currentIndex + 1
}
}
}
}
struct TransactionState {
let visibleSize: CGSize
let items: [ListViewItem]
}
struct PendingNode {
let index: Int
let node: QueueLocalObject<ListViewItemNode>
let apply: () -> (Signal<Void, NoError>?, () -> Void)
let frame: CGRect
let apparentHeight: CGFloat
}
enum ListViewStateNode {
case Node(index: Int, frame: CGRect, referenceNode: QueueLocalObject<ListViewItemNode>?, newNode: QueueLocalObject<ListViewItemNode>?)
case Placeholder(frame: CGRect)
var index: Int? {
switch self {
case let .Node(index, _, _, _):
return index
case .Placeholder(_):
return nil
}
}
var frame: CGRect {
get {
switch self {
case let .Node(_, frame, _, _):
return frame
case .Placeholder(let frame):
return frame
}
} set(value) {
switch self {
case let .Node(index, _, referenceNode, newNode):
self = .Node(index: index, frame: value, referenceNode: referenceNode, newNode: newNode)
case .Placeholder(_):
self = .Placeholder(frame: value)
}
}
}
}
enum ListViewInsertionOffsetDirection {
case Up
case Down
init(_ hint: ListViewItemOperationDirectionHint) {
switch hint {
case .Up:
self = .Up
case .Down:
self = .Down
}
}
func inverted() -> ListViewInsertionOffsetDirection {
switch self {
case .Up:
return .Down
case .Down:
return .Up
}
}
}
struct ListViewInsertionPoint {
let index: Int
let point: CGPoint
let direction: ListViewInsertionOffsetDirection
}
struct ListViewState {
var insets: UIEdgeInsets
var itemOffsetInsets: UIEdgeInsets
var visibleSize: CGSize
let invisibleInset: CGFloat
var nodes: [ListViewStateNode]
var scrollPosition: (Int, ListViewScrollPosition)?
var stationaryOffset: (Int, CGFloat)?
let stackFromBottom: Bool
mutating func fixScrollPosition(_ itemCount: Int) {
if let (fixedIndex, fixedPosition) = self.scrollPosition {
for node in self.nodes {
if let index = node.index, index == fixedIndex {
var offset: CGFloat
switch fixedPosition {
case let .bottom(additionalOffset):
offset = (self.visibleSize.height - self.insets.bottom) - node.frame.maxY + additionalOffset
case let .top(additionalOffset):
offset = self.insets.top - node.frame.minY + additionalOffset
case let .center(overflow):
let contentAreaHeight = self.visibleSize.height - self.insets.bottom - self.insets.top
if node.frame.size.height <= contentAreaHeight + CGFloat.ulpOfOne {
offset = self.insets.top + floor((contentAreaHeight - node.frame.size.height) / 2.0) - node.frame.minY
} else {
switch overflow {
case .top:
offset = self.insets.top - node.frame.minY
case .bottom:
offset = (self.visibleSize.height - self.insets.bottom) - node.frame.maxY
case let .custom(getOverflow):
if Thread.isMainThread, case let .Node(_, _, referenceNode, newNode) = node, let listNode = referenceNode?.syncWith({ $0 }) ?? newNode?.syncWith({ $0 }) {
let overflow = getOverflow(listNode)
if overflow == 0.0 {
offset = self.insets.top - node.frame.minY
} else {
offset = (self.visibleSize.height - self.insets.bottom) - node.frame.maxY
offset += overflow
offset -= floor((self.visibleSize.height - self.insets.bottom - self.insets.top) * 0.5)
}
} else {
offset = self.insets.top - node.frame.minY
}
}
}
case .visible:
if node.frame.maxY > self.visibleSize.height - self.insets.bottom {
offset = (self.visibleSize.height - self.insets.bottom) - node.frame.maxY
} else if node.frame.minY < self.insets.top {
offset = self.insets.top - node.frame.minY
} else {
offset = 0.0
}
}
var minY: CGFloat = CGFloat.greatestFiniteMagnitude
var maxY: CGFloat = 0.0
for i in 0 ..< self.nodes.count {
var frame = self.nodes[i].frame
frame = frame.offsetBy(dx: 0.0, dy: offset)
self.nodes[i].frame = frame
minY = min(minY, frame.minY)
maxY = max(maxY, frame.maxY)
}
var additionalOffset: CGFloat = 0.0
if minY > self.insets.top {
additionalOffset = self.insets.top - minY
}
if abs(additionalOffset) > CGFloat.ulpOfOne {
for i in 0 ..< self.nodes.count {
var frame = self.nodes[i].frame
frame = frame.offsetBy(dx: 0.0, dy: additionalOffset)
self.nodes[i].frame = frame
}
}
self.snapToBounds(itemCount, snapTopItem: true, stackFromBottom: self.stackFromBottom)
break
}
}
} else if let (stationaryIndex, stationaryOffset) = self.stationaryOffset {
for node in self.nodes {
if node.index == stationaryIndex {
let offset = stationaryOffset - node.frame.minY
if abs(offset) > CGFloat.ulpOfOne {
for i in 0 ..< self.nodes.count {
var frame = self.nodes[i].frame
frame = frame.offsetBy(dx: 0.0, dy: offset)
self.nodes[i].frame = frame
}
}
break
}
}
}
}
mutating func setupStationaryOffset(_ index: Int, boundary: Int, frames: [Int: CGRect]) {
if index < boundary {
for node in self.nodes {
if let nodeIndex = node.index , nodeIndex >= index {
if let frame = frames[nodeIndex] {
self.stationaryOffset = (nodeIndex, frame.minY)
break
}
}
}
} else {
for node in self.nodes.reversed() {
if let nodeIndex = node.index , nodeIndex <= index {
if let frame = frames[nodeIndex] {
self.stationaryOffset = (nodeIndex, frame.minY)
break
}
}
}
}
}
mutating func snapToBounds(_ itemCount: Int, snapTopItem: Bool, stackFromBottom: Bool) {
var completeHeight: CGFloat = 0.0
var topItemFound = false
var bottomItemFound = false
var topItemEdge: CGFloat = 0.0
var bottomItemEdge: CGFloat = 0.0
for node in self.nodes {
if let index = node.index {
if index == 0 {
topItemFound = true
topItemEdge = node.frame.minY
}
break
}
}
for node in self.nodes.reversed() {
if let index = node.index {
if index == itemCount - 1 {
bottomItemFound = true
bottomItemEdge = node.frame.maxY
}
break
}
}
if topItemFound && bottomItemFound {
for node in self.nodes {
completeHeight += node.frame.size.height
}
}
let overscroll: CGFloat = 0.0
var offset: CGFloat = 0.0
if topItemFound && bottomItemFound {
let areaHeight = min(completeHeight, self.visibleSize.height - self.insets.bottom - self.insets.top)
if bottomItemEdge < self.insets.top + areaHeight - overscroll {
offset = self.insets.top + areaHeight - overscroll - bottomItemEdge
} else if topItemEdge > self.insets.top - overscroll && snapTopItem {
offset = (self.insets.top - overscroll) - topItemEdge
}
} else if topItemFound {
if topItemEdge > self.insets.top - overscroll && snapTopItem {
offset = (self.insets.top - overscroll) - topItemEdge
}
} else if bottomItemFound {
if bottomItemEdge < self.visibleSize.height - self.insets.bottom - overscroll {
offset = self.visibleSize.height - self.insets.bottom - overscroll - bottomItemEdge
}
}
if abs(offset) > CGFloat.ulpOfOne {
for i in 0 ..< self.nodes.count {
var frame = self.nodes[i].frame
frame.origin.y += offset
self.nodes[i].frame = frame
}
}
}
func insertionPoint(_ insertDirectionHints: [Int: ListViewItemOperationDirectionHint], itemCount: Int) -> ListViewInsertionPoint? {
var fixedNode: (nodeIndex: Int, index: Int, frame: CGRect)?
if let (fixedIndex, _) = self.scrollPosition {
for i in 0 ..< self.nodes.count {
let node = self.nodes[i]
if let index = node.index , index == fixedIndex {
fixedNode = (i, index, node.frame)
break
}
}
if fixedNode == nil {
return ListViewInsertionPoint(index: fixedIndex, point: CGPoint(), direction: .Down)
}
}
var fixedNodeIsStationary = false
if fixedNode == nil {
if let (fixedIndex, _) = self.stationaryOffset {
for i in 0 ..< self.nodes.count {
let node = self.nodes[i]
if let index = node.index , index == fixedIndex {
fixedNode = (i, index, node.frame)
fixedNodeIsStationary = true
break
}
}
}
}
if fixedNode == nil {
for i in 0 ..< self.nodes.count {
let node = self.nodes[i]
if let index = node.index , node.frame.maxY >= self.itemOffsetInsets.top {
fixedNode = (i, index, node.frame)
break
}
}
}
if fixedNode == nil && self.nodes.count != 0 {
for i in (0 ..< self.nodes.count).reversed() {
let node = self.nodes[i]
if let index = node.index {
fixedNode = (i, index, node.frame)
break
}
}
}
if let fixedNode = fixedNode {
var currentUpperNode = fixedNode
for i in (0 ..< fixedNode.nodeIndex).reversed() {
let node = self.nodes[i]
if let index = node.index {
if index != currentUpperNode.index - 1 {
if currentUpperNode.frame.minY > -self.invisibleInset - CGFloat.ulpOfOne {
var directionHint: ListViewInsertionOffsetDirection?
if let hint = insertDirectionHints[currentUpperNode.index - 1] , currentUpperNode.frame.minY > self.itemOffsetInsets.top - CGFloat.ulpOfOne {
directionHint = ListViewInsertionOffsetDirection(hint)
}
return ListViewInsertionPoint(index: currentUpperNode.index - 1, point: CGPoint(x: 0.0, y: currentUpperNode.frame.minY), direction: directionHint ?? .Up)
} else {
break
}
}
currentUpperNode = (i, index, node.frame)
}
}
if currentUpperNode.index != 0 && currentUpperNode.frame.minY > -self.invisibleInset - CGFloat.ulpOfOne {
var directionHint: ListViewInsertionOffsetDirection?
if let hint = insertDirectionHints[currentUpperNode.index - 1] {
if currentUpperNode.frame.minY >= self.itemOffsetInsets.top - CGFloat.ulpOfOne {
directionHint = ListViewInsertionOffsetDirection(hint)
}
} else if currentUpperNode.frame.minY >= self.itemOffsetInsets.top - CGFloat.ulpOfOne && !fixedNodeIsStationary {
directionHint = .Down
}
return ListViewInsertionPoint(index: currentUpperNode.index - 1, point: CGPoint(x: 0.0, y: currentUpperNode.frame.minY), direction: directionHint ?? .Up)
}
var currentLowerNode = fixedNode
if fixedNode.nodeIndex + 1 < self.nodes.count {
for i in (fixedNode.nodeIndex + 1) ..< self.nodes.count {
let node = self.nodes[i]
if let index = node.index {
if index != currentLowerNode.index + 1 {
if currentLowerNode.frame.maxY < self.visibleSize.height + self.invisibleInset - CGFloat.ulpOfOne {
var directionHint: ListViewInsertionOffsetDirection?
if let hint = insertDirectionHints[currentLowerNode.index + 1] , currentLowerNode.frame.maxY < self.visibleSize.height - self.itemOffsetInsets.bottom + CGFloat.ulpOfOne {
directionHint = ListViewInsertionOffsetDirection(hint)
}
return ListViewInsertionPoint(index: currentLowerNode.index + 1, point: CGPoint(x: 0.0, y: currentLowerNode.frame.maxY), direction: directionHint ?? .Down)
} else {
break
}
}
currentLowerNode = (i, index, node.frame)
}
}
}
if currentLowerNode.index != itemCount - 1 && currentLowerNode.frame.maxY < self.visibleSize.height + self.invisibleInset - CGFloat.ulpOfOne {
var directionHint: ListViewInsertionOffsetDirection?
if let hint = insertDirectionHints[currentLowerNode.index + 1] , currentLowerNode.frame.maxY < self.visibleSize.height - self.itemOffsetInsets.bottom + CGFloat.ulpOfOne {
directionHint = ListViewInsertionOffsetDirection(hint)
}
return ListViewInsertionPoint(index: currentLowerNode.index + 1, point: CGPoint(x: 0.0, y: currentLowerNode.frame.maxY), direction: directionHint ?? .Down)
}
} else if itemCount != 0 {
return ListViewInsertionPoint(index: 0, point: CGPoint(x: 0.0, y: self.insets.top), direction: .Down)
}
return nil
}
mutating func removeInvisibleNodes(_ operations: inout [ListViewStateOperation]) {
var i = 0
var visibleItemNodeHeight: CGFloat = 0.0
while i < self.nodes.count {
visibleItemNodeHeight += self.nodes[i].frame.height
i += 1
}
if visibleItemNodeHeight > (self.visibleSize.height + self.invisibleInset + self.invisibleInset) {
i = self.nodes.count - 1
while i >= 0 {
let itemNode = self.nodes[i]
let frame = itemNode.frame
//print("node \(i) frame \(frame)")
if frame.maxY < -self.invisibleInset || frame.origin.y > self.visibleSize.height + self.invisibleInset {
//print("remove invisible 1 \(i) frame \(frame)")
operations.append(.Remove(index: i, offsetDirection: frame.maxY < -self.invisibleInset ? .Down : .Up))
self.nodes.remove(at: i)
}
i -= 1
}
}
let upperBound = -self.invisibleInset + CGFloat.ulpOfOne
for i in 0 ..< self.nodes.count {
let node = self.nodes[i]
if let index = node.index , node.frame.maxY > upperBound {
if i != 0 {
var previousIndex = index
for j in (0 ..< i).reversed() {
if self.nodes[j].frame.maxY < upperBound {
if let index = self.nodes[j].index {
if index != previousIndex - 1 {
//print("remove monotonity \(j) (\(index))")
operations.append(.Remove(index: j, offsetDirection: .Down))
self.nodes.remove(at: j)
} else {
previousIndex = index
}
}
}
}
}
break
}
}
let lowerBound = self.visibleSize.height + self.invisibleInset - CGFloat.ulpOfOne
for i in (0 ..< self.nodes.count).reversed() {
let node = self.nodes[i]
if let index = node.index , node.frame.minY < lowerBound {
if i != self.nodes.count - 1 {
var previousIndex = index
var removeIndices: [Int] = []
for j in (i + 1) ..< self.nodes.count {
if self.nodes[j].frame.minY > lowerBound {
if let index = self.nodes[j].index {
if index != previousIndex + 1 {
removeIndices.append(j)
} else {
previousIndex = index
}
}
}
}
if !removeIndices.isEmpty {
for i in removeIndices.reversed() {
//print("remove monotonity \(i) (\(self.nodes[i].index!))")
operations.append(.Remove(index: i, offsetDirection: .Up))
self.nodes.remove(at: i)
}
}
}
break
}
}
}
func nodeInsertionPointAndIndex(_ itemIndex: Int) -> (CGPoint, Int) {
if self.nodes.count == 0 {
return (CGPoint(x: 0.0, y: self.insets.top), 0)
} else {
var index = 0
var lastNodeWithIndex = -1
for node in self.nodes {
if let nodeItemIndex = node.index {
if nodeItemIndex > itemIndex {
break
}
lastNodeWithIndex = index
}
index += 1
}
lastNodeWithIndex += 1
return (CGPoint(x: 0.0, y: lastNodeWithIndex == 0 ? self.nodes[0].frame.minY : self.nodes[lastNodeWithIndex - 1].frame.maxY), lastNodeWithIndex)
}
}
func continuousHeightRelativeToNodeIndex(_ fixedNodeIndex: Int) -> CGFloat {
let fixedIndex = self.nodes[fixedNodeIndex].index!
var height: CGFloat = 0.0
if fixedNodeIndex != 0 {
var upperIndex = fixedIndex
for i in (0 ..< fixedNodeIndex).reversed() {
if let index = self.nodes[i].index {
if index == upperIndex - 1 {
height += self.nodes[i].frame.size.height
upperIndex = index
} else {
break
}
}
}
}
if fixedNodeIndex != self.nodes.count - 1 {
var lowerIndex = fixedIndex
for i in (fixedNodeIndex + 1) ..< self.nodes.count {
if let index = self.nodes[i].index {
if index == lowerIndex + 1 {
height += self.nodes[i].frame.size.height
lowerIndex = index
} else {
break
}
}
}
}
return height
}
mutating func insertNode(_ itemIndex: Int, node: QueueLocalObject<ListViewItemNode>, layout: ListViewItemNodeLayout, apply: @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void), offsetDirection: ListViewInsertionOffsetDirection, animated: Bool, operations: inout [ListViewStateOperation], itemCount: Int) {
let (insertionOrigin, insertionIndex) = self.nodeInsertionPointAndIndex(itemIndex)
let nodeOrigin: CGPoint
switch offsetDirection {
case .Up:
nodeOrigin = CGPoint(x: insertionOrigin.x, y: insertionOrigin.y - (animated ? 0.0 : layout.size.height))
case .Down:
nodeOrigin = insertionOrigin
}
let nodeFrame = CGRect(origin: nodeOrigin, size: CGSize(width: layout.size.width, height: animated ? 0.0 : layout.size.height))
operations.append(.InsertNode(index: insertionIndex, offsetDirection: offsetDirection, animated: animated, node: node, layout: layout, apply: apply))
self.nodes.insert(.Node(index: itemIndex, frame: nodeFrame, referenceNode: nil, newNode: node), at: insertionIndex)
if !animated {
switch offsetDirection {
case .Up:
var i = insertionIndex - 1
while i >= 0 {
var frame = self.nodes[i].frame
frame.origin.y -= nodeFrame.size.height
self.nodes[i].frame = frame
i -= 1
}
case .Down:
var i = insertionIndex + 1
while i < self.nodes.count {
var frame = self.nodes[i].frame
frame.origin.y += nodeFrame.size.height
self.nodes[i].frame = frame
i += 1
}
}
}
var previousIndex: Int?
for node in self.nodes {
if let index = node.index {
if let currentPreviousIndex = previousIndex {
if index <= currentPreviousIndex {
print("index <= previousIndex + 1")
break
}
previousIndex = index
} else {
previousIndex = index
}
}
}
if let _ = self.scrollPosition {
self.fixScrollPosition(itemCount)
}
}
mutating func removeNodeAtIndex(_ index: Int, direction: ListViewItemOperationDirectionHint?, animated: Bool, operations: inout [ListViewStateOperation]) {
let node = self.nodes[index]
if case let .Node(_, _, referenceNode, _) = node {
let nodeFrame = node.frame
self.nodes.remove(at: index)
let offsetDirection: ListViewInsertionOffsetDirection
if let direction = direction {
offsetDirection = ListViewInsertionOffsetDirection(direction)
} else {
if nodeFrame.maxY < self.itemOffsetInsets.top + CGFloat.ulpOfOne {
offsetDirection = .Down
} else {
offsetDirection = .Up
}
}
operations.append(.Remove(index: index, offsetDirection: offsetDirection))
if let referenceNode = referenceNode, animated {
self.nodes.insert(.Placeholder(frame: nodeFrame), at: index)
operations.append(.InsertDisappearingPlaceholder(index: index, referenceNode: referenceNode, offsetDirection: offsetDirection.inverted()))
} else {
if nodeFrame.maxY > self.itemOffsetInsets.top - CGFloat.ulpOfOne {
if let direction = direction , direction == .Down && node.frame.minY < self.visibleSize.height - self.itemOffsetInsets.bottom + CGFloat.ulpOfOne {
for i in (0 ..< index).reversed() {
var frame = self.nodes[i].frame
frame.origin.y += nodeFrame.size.height
self.nodes[i].frame = frame
}
} else {
for i in index ..< self.nodes.count {
var frame = self.nodes[i].frame
frame.origin.y -= nodeFrame.size.height
self.nodes[i].frame = frame
}
}
} else if index != 0 {
for i in (0 ..< index).reversed() {
var frame = self.nodes[i].frame
frame.origin.y += nodeFrame.size.height
self.nodes[i].frame = frame
}
}
}
} else {
assertionFailure()
}
}
mutating func updateNodeAtItemIndex(_ itemIndex: Int, layout: ListViewItemNodeLayout, direction: ListViewItemOperationDirectionHint?, isAnimated: Bool, apply: @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void), operations: inout [ListViewStateOperation]) {
var i = -1
for node in self.nodes {
i += 1
if node.index == itemIndex {
if !isAnimated {
let offsetDirection: ListViewInsertionOffsetDirection
if let direction = direction {
offsetDirection = ListViewInsertionOffsetDirection(direction)
} else {
if node.frame.maxY < self.itemOffsetInsets.top + CGFloat.ulpOfOne {
offsetDirection = .Down
} else {
offsetDirection = .Up
}
}
switch offsetDirection {
case .Up:
let offsetDelta = -(layout.size.height - node.frame.size.height)
var updatedFrame = node.frame
updatedFrame.origin.y += offsetDelta
updatedFrame.size.height = layout.size.height
self.nodes[i].frame = updatedFrame
for j in 0 ..< i {
var frame = self.nodes[j].frame
frame.origin.y += offsetDelta
self.nodes[j].frame = frame
}
case .Down:
let offsetDelta = layout.size.height - node.frame.size.height
var updatedFrame = node.frame
updatedFrame.size.height = layout.size.height
self.nodes[i].frame = updatedFrame
for j in i + 1 ..< self.nodes.count {
var frame = self.nodes[j].frame
frame.origin.y += offsetDelta
self.nodes[j].frame = frame
}
}
operations.append(.UpdateLayout(index: i, layout: layout, apply: apply))
} else {
operations.append(.UpdateLayout(index: i, layout: layout, apply: apply))
}
break
}
}
}
}
enum ListViewStateOperation {
case InsertNode(index: Int, offsetDirection: ListViewInsertionOffsetDirection, animated: Bool, node: QueueLocalObject<ListViewItemNode>, layout: ListViewItemNodeLayout, apply: () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void))
case InsertDisappearingPlaceholder(index: Int, referenceNode: QueueLocalObject<ListViewItemNode>, offsetDirection: ListViewInsertionOffsetDirection)
case Remove(index: Int, offsetDirection: ListViewInsertionOffsetDirection)
case Remap([Int: Int])
case UpdateLayout(index: Int, layout: ListViewItemNodeLayout, apply: () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void))
}
@@ -0,0 +1,106 @@
import Foundation
import UIKit
import SwiftSignalKit
public enum ListViewItemUpdateAnimation {
case None
case System(duration: Double, transition: ControlledTransition)
case Crossfade
public var isAnimated: Bool {
if case .None = self {
return false
} else {
return true
}
}
public var animator: ControlledTransitionAnimator {
switch self {
case .None:
return ControlledTransition.LegacyAnimator(duration: 0.0, curve: .linear)
case let .System(_, transition):
return transition.animator
case .Crossfade:
return ControlledTransition.LegacyAnimator(duration: 0.0, curve: .linear)
}
}
public var transition: ContainedViewLayoutTransition {
switch self {
case .None, .Crossfade:
return .immediate
case let .System(_, transition):
return transition.legacyAnimator.transition
}
}
}
public struct ListViewItemConfigureNodeFlags: OptionSet {
public var rawValue: Int32
public init() {
self.rawValue = 0
}
public init(rawValue: Int32) {
self.rawValue = rawValue
}
public static let preferSynchronousResourceLoading = ListViewItemConfigureNodeFlags(rawValue: 1 << 0)
}
public final class ListViewItemApply {
public private(set) var isOnScreen: Bool
public let timestamp: Double?
public private(set) var invertOffsetDirection: Bool = false
public init(isOnScreen: Bool, timestamp: Double? = nil) {
self.isOnScreen = isOnScreen
self.timestamp = timestamp
}
public func setInvertOffsetDirection() {
self.invertOffsetDirection = true
}
public func setIsOffscreen() {
self.isOnScreen = false
}
}
public protocol ListViewItem {
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void)
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void)
var accessoryItem: ListViewAccessoryItem? { get }
var headerAccessoryItem: ListViewAccessoryItem? { get }
var selectable: Bool { get }
var approximateHeight: CGFloat { get }
func selected(listView: ListView)
}
public extension ListViewItem {
var accessoryItem: ListViewAccessoryItem? {
return nil
}
var headerAccessoryItem: ListViewAccessoryItem? {
return nil
}
var selectable: Bool {
return false
}
var approximateHeight: CGFloat {
return 44.0
}
func selected(listView: ListView) {
}
func performSecondaryAction(listView: ListView) {
}
}
@@ -0,0 +1,165 @@
import Foundation
import UIKit
import AsyncDisplayKit
public enum ListViewItemHeaderStickDirection {
case top
case topEdge
case bottom
}
public protocol ListViewItemHeader: AnyObject {
var id: ListViewItemNode.HeaderId { get }
var stackingId: ListViewItemNode.HeaderId? { get }
var stickDirection: ListViewItemHeaderStickDirection { get }
var height: CGFloat { get }
var stickOverInsets: Bool { get }
func combinesWith(other: ListViewItemHeader) -> Bool
func node(synchronousLoad: Bool) -> ListViewItemHeaderNode
func updateNode(_ node: ListViewItemHeaderNode, previous: ListViewItemHeader?, next: ListViewItemHeader?)
}
open class ListViewItemHeaderNode: ASDisplayNode {
private final var spring: ListViewItemSpring?
let wantsScrollDynamics: Bool
let isRotated: Bool
final private(set) var internalStickLocationDistanceFactor: CGFloat = 0.0
final var internalStickLocationDistance: CGFloat = 0.0
private var isFlashingOnScrolling = false
weak var attachedToItemNode: ListViewItemNode?
var offsetByHeaderNodeId: ListViewItemNode.HeaderId?
var naturalOriginY: CGFloat?
public var item: ListViewItemHeader?
func updateInternalStickLocationDistanceFactor(_ factor: CGFloat, animated: Bool) {
self.internalStickLocationDistanceFactor = factor
}
final func updateFlashingOnScrollingInternal(_ isFlashingOnScrolling: Bool, animated: Bool) {
if self.isFlashingOnScrolling != isFlashingOnScrolling {
self.isFlashingOnScrolling = isFlashingOnScrolling
self.updateFlashingOnScrolling(isFlashingOnScrolling, animated: animated)
}
}
open func updateFlashingOnScrolling(_ isFlashingOnScrolling: Bool, animated: Bool) {
}
open func getEffectiveAlpha() -> CGFloat {
return self.alpha
}
public init(layerBacked: Bool = false, dynamicBounce: Bool = false, isRotated: Bool = false, seeThrough: Bool = false) {
self.wantsScrollDynamics = dynamicBounce
self.isRotated = isRotated
if dynamicBounce {
self.spring = ListViewItemSpring(stiffness: -280.0, damping: -24.0, mass: 0.85)
}
super.init()
self.isLayerBacked = layerBacked
}
open func updateStickDistanceFactor(_ factor: CGFloat, distance: CGFloat, transition: ContainedViewLayoutTransition) {
}
final func addScrollingOffset(_ scrollingOffset: CGFloat) {
if self.spring != nil && internalStickLocationDistanceFactor.isZero {
let bounds = self.bounds
self.bounds = CGRect(origin: CGPoint(x: 0.0, y: bounds.origin.y + scrollingOffset), size: bounds.size)
}
}
public func animate(_ timestamp: Double) -> Bool {
var continueAnimations = false
if let _ = self.spring {
let bounds = self.bounds
var offset = bounds.origin.y
let currentOffset = offset
let frictionConstant: CGFloat = testSpringFriction
let springConstant: CGFloat = testSpringConstant
let time: CGFloat = 1.0 / 60.0
// friction force = velocity * friction constant
let frictionForce = self.spring!.velocity * frictionConstant
// spring force = (target point - current position) * spring constant
let springForce = -currentOffset * springConstant
// force = spring force - friction force
let force = springForce - frictionForce
// velocity = current velocity + force * time / mass
self.spring!.velocity = self.spring!.velocity + force * time
// position = current position + velocity * time
offset = currentOffset + self.spring!.velocity * time
offset = offset.isNaN ? 0.0 : offset
let epsilon: CGFloat = 0.1
if abs(offset) < epsilon && abs(self.spring!.velocity) < epsilon {
offset = 0.0
self.spring!.velocity = 0.0
} else {
continueAnimations = true
}
if abs(offset) > 250.0 {
offset = offset < 0.0 ? -250.0 : 250.0
}
self.bounds = CGRect(origin: CGPoint(x: 0.0, y: offset), size: bounds.size)
}
return continueAnimations
}
open func animateRemoved(duration: Double) {
self.alpha = 0.0
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false)
self.layer.animateScale(from: 1.0, to: 0.2, duration: duration, removeOnCompletion: false)
}
open func animateAdded(duration: Double) {
self.layer.animateAlpha(from: 0.0, to: self.alpha, duration: 0.2)
self.layer.animateScale(from: 0.2, to: 1.0, duration: 0.2)
}
private var cachedLayout: (CGSize, CGFloat, CGFloat)?
public func updateLayoutInternal(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
var update = false
if let cachedLayout = self.cachedLayout {
if cachedLayout.0 != size || cachedLayout.1 != leftInset || cachedLayout.2 != rightInset {
update = true
}
} else {
update = true
}
if update {
self.cachedLayout = (size, leftInset, rightInset)
self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: transition)
}
}
open func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
}
open func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
}
public func updateFrame(_ frame: CGRect, within containerSize: CGSize, updateFrame: Bool = true) {
if updateFrame {
self.frame = frame
}
if frame.maxY < 0.0 || frame.minY > containerSize.height {
} else {
self.updateAbsoluteRect(frame, within: containerSize)
}
}
}
@@ -0,0 +1,673 @@
import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
var testSpringFrictionLimits: (CGFloat, CGFloat) = (3.0, 60.0)
var testSpringFriction: CGFloat = 31.8211269378662
var testSpringConstantLimits: (CGFloat, CGFloat) = (3.0, 450.0)
var testSpringConstant: CGFloat = 443.704223632812
var testSpringResistanceFreeLimits: (CGFloat, CGFloat) = (0.05, 1.0)
var testSpringFreeResistance: CGFloat = 0.676197171211243
var testSpringResistanceScrollingLimits: (CGFloat, CGFloat) = (0.1, 1.0)
var testSpringScrollingResistance: CGFloat = 0.6721
public struct ListViewItemAnimationOptions {
public let short: Bool
public let invertOffsetDirection: Bool
public init(short: Bool = false, invertOffsetDirection: Bool = false) {
self.short = short
self.invertOffsetDirection = invertOffsetDirection
}
}
struct ListViewItemSpring {
let stiffness: CGFloat
let damping: CGFloat
let mass: CGFloat
var velocity: CGFloat = 0.0
init(stiffness: CGFloat, damping: CGFloat, mass: CGFloat) {
self.stiffness = stiffness
self.damping = damping
self.mass = mass
}
}
public struct ListViewItemNodeLayout {
public let contentSize: CGSize
public let insets: UIEdgeInsets
public init() {
self.contentSize = CGSize()
self.insets = UIEdgeInsets()
}
public init(contentSize: CGSize, insets: UIEdgeInsets) {
self.contentSize = contentSize
self.insets = insets
}
public var size: CGSize {
return CGSize(width: self.contentSize.width + self.insets.left + self.insets.right, height: self.contentSize.height + self.insets.top + self.insets.bottom)
}
}
public enum ListViewItemNodeVisibility: Equatable {
case none
case visible(CGFloat, CGRect)
}
public struct ListViewItemLayoutParams: Equatable {
public let width: CGFloat
public let leftInset: CGFloat
public let rightInset: CGFloat
public let availableHeight: CGFloat
public let isStandalone: Bool
public init(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, availableHeight: CGFloat, isStandalone: Bool = false) {
self.width = width
self.leftInset = leftInset
self.rightInset = rightInset
self.availableHeight = availableHeight
self.isStandalone = isStandalone
}
}
private final class ControlledTransitionContext {
let transition: ControlledTransition
let beginAt: Double
init(transition: ControlledTransition, beginAt: Double) {
self.transition = transition
self.beginAt = beginAt
}
}
open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode {
public struct HeaderId: Hashable {
public var space: AnyHashable
public var id: AnyHashable
public init(space: AnyHashable, id: AnyHashable) {
self.space = space
self.id = id
}
}
let rotated: Bool
public internal(set) final var index: Int?
public var isHighlightedInOverlay: Bool = false
public private(set) var accessoryItemNode: ListViewAccessoryItemNode?
func setAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode?, leftInset: CGFloat, rightInset: CGFloat) {
self.accessoryItemNode = accessoryItemNode
if let accessoryItemNode = accessoryItemNode {
self.layoutAccessoryItemNode(accessoryItemNode, leftInset: leftInset, rightInset: rightInset)
}
}
open func addAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) {
self.addSubnode(accessoryItemNode)
}
final var headerAccessoryItemNode: ListViewAccessoryItemNode? {
didSet {
if let headerAccessoryItemNode = self.headerAccessoryItemNode {
self.layoutHeaderAccessoryItemNode(headerAccessoryItemNode)
}
}
}
open var extractedBackgroundNode: ASDisplayNode? {
return nil
}
private final var spring: ListViewItemSpring?
private final var animations: [(String, ListViewAnimation)] = []
private final var pendingControlledTransitions: [ControlledTransition] = []
private final var controlledTransitions: [ControlledTransitionContext] = []
final var tempHeaderSpaceAffinities: [ListViewItemNode.HeaderId: Int] = [:]
final var headerSpaceAffinities: [ListViewItemNode.HeaderId: Int] = [:]
public internal(set) var attachedHeaderNodes: [ListViewItemHeaderNode] = []
open func attachedHeaderNodesUpdated() {
}
final let wantsScrollDynamics: Bool
open var preferredAnimationCurve: (CGFloat) -> CGFloat {
return listViewAnimationCurveSystem
}
public final var wantsTrailingItemSpaceUpdates: Bool = false
public final var scrollPositioningInsets: UIEdgeInsets = UIEdgeInsets()
public final var canBeUsedAsScrollToItemAnchor: Bool = true
open var visibility: ListViewItemNodeVisibility = .none
open var canBeSelected: Bool {
return true
}
open func visibleForSelection(at point: CGPoint) -> Bool {
return true
}
open var canBeLongTapped: Bool {
return false
}
open var preventsTouchesToOtherItems: Bool {
return false
}
open func touchesToOtherItemsPrevented() {
}
open func tapped() {
}
open func longTapped() {
}
public final var insets: UIEdgeInsets = UIEdgeInsets() {
didSet {
let effectiveInsets = self.insets
self.frame = CGRect(origin: self.frame.origin, size: CGSize(width: self.contentSize.width, height: self.contentSize.height + effectiveInsets.top + effectiveInsets.bottom))
let bounds = self.bounds
self.bounds = CGRect(origin: CGPoint(x: bounds.origin.x, y: -effectiveInsets.top + self.contentOffset + self.transitionOffset), size: bounds.size)
}
}
private final var _contentSize: CGSize = CGSize()
public final var contentSize: CGSize {
get {
return self._contentSize
} set(value) {
let effectiveInsets = self.insets
self.frame = CGRect(origin: self.frame.origin, size: CGSize(width: value.width, height: value.height + effectiveInsets.top + effectiveInsets.bottom))
}
}
private var contentOffset: CGFloat = 0.0 {
didSet {
let effectiveInsets = self.insets
let bounds = self.bounds
self.bounds = CGRect(origin: CGPoint(x: bounds.origin.x, y: -effectiveInsets.top + self.contentOffset + self.transitionOffset), size: bounds.size)
}
}
public var transitionOffset: CGFloat = 0.0 {
didSet {
let effectiveInsets = self.insets
let bounds = self.bounds
self.bounds = CGRect(origin: CGPoint(x: bounds.origin.x, y: -effectiveInsets.top + self.contentOffset + self.transitionOffset), size: bounds.size)
}
}
public var layout: ListViewItemNodeLayout {
var insets = self.insets
var contentSize = self.contentSize
if let animation = self.animationForKey("insets") {
insets = animation.to as! UIEdgeInsets
}
if let animation = self.animationForKey("apparentHeight") {
contentSize.height = (animation.to as! CGFloat) - insets.top - insets.bottom
}
return ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
}
public var displayResourcesReady: Signal<Void, NoError> {
return .complete()
}
public init(layerBacked: Bool, dynamicBounce: Bool = true, rotated: Bool = false, seeThrough: Bool = false) {
if dynamicBounce {
self.spring = ListViewItemSpring(stiffness: -280.0, damping: -24.0, mass: 0.85)
}
self.wantsScrollDynamics = dynamicBounce
self.rotated = rotated
super.init()
self.isLayerBacked = layerBacked
}
open var apparentHeight: CGFloat = 0.0
public private(set) var apparentHeightTransition: (CGFloat, CGFloat)?
private var _bounds: CGRect = CGRect()
private var _position: CGPoint = CGPoint()
open override var frame: CGRect {
get {
return CGRect(origin: CGPoint(x: self._position.x - self._bounds.width / 2.0, y: self._position.y - self._bounds.height / 2.0), size: self._bounds.size)
} set(value) {
let previousSize = self._bounds.size
super.frame = value
self._bounds.size = value.size
self._position = CGPoint(x: value.midX, y: value.midY)
let effectiveInsets = self.insets
self._contentSize = CGSize(width: value.size.width, height: value.size.height - effectiveInsets.top - effectiveInsets.bottom)
if previousSize != value.size {
if let headerAccessoryItemNode = self.headerAccessoryItemNode {
self.layoutHeaderAccessoryItemNode(headerAccessoryItemNode)
}
}
}
}
open override var bounds: CGRect {
get {
return self._bounds
} set(value) {
let previousSize = self._bounds.size
super.bounds = value
self._bounds = value
let effectiveInsets = self.insets
self._contentSize = CGSize(width: value.size.width, height: value.size.height - effectiveInsets.top - effectiveInsets.bottom)
if previousSize != value.size {
if let headerAccessoryItemNode = self.headerAccessoryItemNode {
self.layoutHeaderAccessoryItemNode(headerAccessoryItemNode)
}
}
}
}
public var contentBounds: CGRect {
let bounds = self.bounds
let effectiveInsets = self.insets
return CGRect(origin: CGPoint(x: 0.0, y: bounds.origin.y + effectiveInsets.top), size: CGSize(width: bounds.size.width, height: bounds.size.height - effectiveInsets.top - effectiveInsets.bottom))
}
open override var position: CGPoint {
get {
return self._position
} set(value) {
super.position = value
self._position = value
}
}
public final var apparentFrame: CGRect {
var frame = self.frame
frame.size.height = self.apparentHeight
return frame
}
public final var apparentContentFrame: CGRect {
var frame = self.frame
let insets = self.insets
frame.origin.y += insets.top
frame.size.height = self.apparentHeight - insets.top - insets.bottom
return frame
}
public final var apparentBounds: CGRect {
var bounds = self.bounds
bounds.size.height = self.apparentHeight
return bounds
}
open func layoutAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode, leftInset: CGFloat, rightInset: CGFloat) {
}
open func layoutHeaderAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) {
}
open func reuse() {
}
final func addScrollingOffset(_ scrollingOffset: CGFloat) {
if self.spring != nil {
self.contentOffset += scrollingOffset
}
}
func initializeDynamicsFromSibling(_ itemView: ListViewItemNode, additionalOffset: CGFloat) {
if let itemViewSpring = itemView.spring {
self.contentOffset = itemView.contentOffset + additionalOffset
self.spring?.velocity = itemViewSpring.velocity
}
}
public func animate(timestamp: Double, invertOffsetDirection: inout Bool) -> Bool {
var continueAnimations = false
if let _ = self.spring {
var offset = self.contentOffset
let frictionConstant: CGFloat = testSpringFriction
let springConstant: CGFloat = testSpringConstant
let time: CGFloat = 1.0 / 60.0
// friction force = velocity * friction constant
let frictionForce = self.spring!.velocity * frictionConstant
// spring force = (target point - current position) * spring constant
let springForce = -self.contentOffset * springConstant
// force = spring force - friction force
let force = springForce - frictionForce
// velocity = current velocity + force * time / mass
self.spring!.velocity = self.spring!.velocity + force * time
// position = current position + velocity * time
offset = self.contentOffset + self.spring!.velocity * time
offset = offset.isNaN ? 0.0 : offset
let epsilon: CGFloat = 0.1
if abs(offset) < epsilon && abs(self.spring!.velocity) < epsilon {
offset = 0.0
self.spring!.velocity = 0.0
} else {
continueAnimations = true
}
if abs(offset) > 250.0 {
offset = offset < 0.0 ? -250.0 : 250.0
}
self.contentOffset = offset
}
var i = 0
var animationCount = self.animations.count
while i < animationCount {
let (_, animation) = self.animations[i]
animation.applyAt(timestamp)
if animation.invertOffsetDirection {
invertOffsetDirection = true
}
if animation.completeAt(timestamp) {
self.animations.remove(at: i)
animationCount -= 1
i -= 1
} else {
continueAnimations = true
}
i += 1
}
i = 0
var transitionCount = self.controlledTransitions.count
while i < transitionCount {
let transition = self.controlledTransitions[i]
var fraction = (timestamp - transition.beginAt) / transition.transition.animator.duration
fraction = max(0.0, min(1.0, fraction))
transition.transition.animator.setAnimationProgress(CGFloat(fraction))
if timestamp >= transition.beginAt + transition.transition.animator.duration {
transition.transition.animator.finishAnimation()
self.controlledTransitions.remove(at: i)
transitionCount -= 1
i -= 1
} else {
continueAnimations = true
}
i += 1
}
if let accessoryItemNode = self.accessoryItemNode {
if (accessoryItemNode.animate(timestamp)) {
continueAnimations = true
}
}
return continueAnimations
}
open func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
}
public func animationForKey(_ key: String) -> ListViewAnimation? {
for (animationKey, animation) in self.animations {
if animationKey == key {
return animation
}
}
return nil
}
public final func setAnimationForKey(_ key: String, animation: ListViewAnimation?) {
for i in 0 ..< self.animations.count {
let (currentKey, currentAnimation) = self.animations[i]
if currentKey == key {
self.animations.remove(at: i)
currentAnimation.cancel()
break
}
}
if let animation = animation {
self.animations.append((key, animation))
}
}
public final func removeAllAnimations() {
let previousAnimations = self.animations
self.animations.removeAll()
for (_, animation) in previousAnimations {
animation.cancel()
}
self.accessoryItemNode?.removeAllAnimations()
for transition in self.controlledTransitions {
transition.transition.animator.finishAnimation()
}
self.controlledTransitions.removeAll()
}
func addPendingControlledTransition(transition: ControlledTransition) {
self.pendingControlledTransitions.append(transition)
}
func beginPendingControlledTransitions(beginAt: Double, forceRestart: Bool) {
for transition in self.pendingControlledTransitions {
self.addControlledTransition(transition: transition, beginAt: beginAt, forceRestart: forceRestart)
}
self.pendingControlledTransitions.removeAll()
}
func addControlledTransition(transition: ControlledTransition, beginAt: Double, forceRestart: Bool) {
for controlledTransition in self.controlledTransitions {
transition.merge(with: controlledTransition.transition, forceRestart: forceRestart)
}
self.controlledTransitions.append(ControlledTransitionContext(transition: transition, beginAt: beginAt))
}
public func addInsetsAnimationToValue(_ value: UIEdgeInsets, duration: Double, beginAt: Double) {
let animation = ListViewAnimation(from: self.insets, to: value, duration: duration, curve: self.preferredAnimationCurve, beginAt: beginAt, update: { [weak self] _, currentValue in
if let strongSelf = self {
strongSelf.insets = currentValue
}
})
self.setAnimationForKey("insets", animation: animation)
}
public func addHeightAnimation(_ value: CGFloat, duration: Double, beginAt: Double, update: ((CGFloat, CGFloat) -> Void)? = nil) {
let animation = ListViewAnimation(from: self.bounds.height, to: value, duration: duration, curve: self.preferredAnimationCurve, beginAt: beginAt, update: { [weak self] progress, currentValue in
if let strongSelf = self {
let frame = strongSelf.frame
strongSelf.frame = CGRect(origin: frame.origin, size: CGSize(width: frame.width, height: currentValue))
if let update = update {
update(progress, currentValue)
}
}
})
self.setAnimationForKey("height", animation: animation)
}
func copyHeightAndApparentHeightAnimations(to otherNode: ListViewItemNode) {
if let animation = self.animationForKey("apparentHeight") {
let updatedAnimation = ListViewAnimation(copying: animation, update: { [weak otherNode] (progress: CGFloat, currentValue: CGFloat) -> Void in
if let strongSelf = otherNode {
let frame = strongSelf.frame
strongSelf.frame = CGRect(origin: frame.origin, size: CGSize(width: frame.width, height: currentValue))
}
})
otherNode.setAnimationForKey("height", animation: updatedAnimation)
}
if let animation = self.animationForKey("apparentHeight") {
let updatedAnimation = ListViewAnimation(copying: animation, update: { [weak otherNode] (progress: CGFloat, currentValue: CGFloat) -> Void in
if let strongSelf = otherNode {
strongSelf.apparentHeight = currentValue
}
})
otherNode.setAnimationForKey("apparentHeight", animation: updatedAnimation)
}
}
public func addApparentHeightAnimation(_ value: CGFloat, duration: Double, beginAt: Double, invertOffsetDirection: Bool = false, update: ((CGFloat, CGFloat) -> Void)? = nil) {
self.apparentHeightTransition = (self.apparentHeight, value)
let animation = ListViewAnimation(from: self.apparentHeight, to: value, duration: duration, invertOffsetDirection: invertOffsetDirection, curve: self.preferredAnimationCurve, beginAt: beginAt, update: { [weak self] progress, currentValue in
if let strongSelf = self {
strongSelf.apparentHeight = currentValue
if let update = update {
update(progress, currentValue)
}
if progress == 1.0 {
strongSelf.apparentHeightTransition = nil
}
}
})
self.setAnimationForKey("apparentHeight", animation: animation)
}
public func modifyApparentHeightAnimation(_ value: CGFloat, beginAt: Double) {
if let previousAnimation = self.animationForKey("apparentHeight") {
var duration = previousAnimation.startTime + previousAnimation.duration - beginAt
if abs(self.apparentHeight - value) < CGFloat.ulpOfOne {
duration = 0.0
}
let animation = ListViewAnimation(from: self.apparentHeight, to: value, duration: duration, curve: self.preferredAnimationCurve, beginAt: beginAt, update: { [weak self] _, currentValue in
if let strongSelf = self {
strongSelf.apparentHeight = currentValue
}
})
self.setAnimationForKey("apparentHeight", animation: animation)
}
}
public func removeApparentHeightAnimation() {
self.setAnimationForKey("apparentHeight", animation: nil)
}
public func addTransitionOffsetAnimation(_ value: CGFloat, duration: Double, beginAt: Double) {
let animation = ListViewAnimation(from: self.transitionOffset, to: value, duration: duration, curve: self.preferredAnimationCurve, beginAt: beginAt, update: { [weak self] _, currentValue in
if let strongSelf = self {
strongSelf.transitionOffset = currentValue
}
})
self.setAnimationForKey("transitionOffset", animation: animation)
}
open func insertionAnimationDuration() -> Double? {
return nil
}
open func updateAnimationDuration() -> Double? {
return nil
}
open func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
}
open func animateAdded(_ currentTimestamp: Double, duration: Double) {
}
open func animateRemoved(_ currentTimestamp: Double, duration: Double) {
}
open func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
}
open func selected() {
}
open func secondaryAction(at point: CGPoint) {
}
open func isReorderable(at point: CGPoint) -> Bool {
return false
}
open func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) {
}
open func shouldAnimateHorizontalFrameTransition() -> Bool {
return false
}
open func headers() -> [ListViewItemHeader]? {
return nil
}
open func updateTrailingItemSpace(_ height: CGFloat, transition: ContainedViewLayoutTransition) {
}
override open func accessibilityElementDidBecomeFocused() {
(self.supernode as? ListView)?.ensureItemNodeVisible(self, animated: false, overflow: 22.0, allowIntersection: true)
}
public func updateFrame(_ frame: CGRect, within containerSize: CGSize, updateFrame: Bool = true, transition: ControlledTransition? = nil) {
if updateFrame {
if let transition {
let previousFrame = self.frame
self.frame = frame
transition.legacyAnimator.transition.animatePositionAdditive(layer: self.layer, offset: CGPoint(x: previousFrame.minX - frame.minX, y: previousFrame.minY - frame.minY))
} else {
self.frame = frame
}
}
if frame.maxY < 0.0 || frame.minY > containerSize.height {
} else {
self.updateAbsoluteRect(frame, within: containerSize)
}
if let extractedBackgroundNode = self.extractedBackgroundNode {
extractedBackgroundNode.frame = frame.offsetBy(dx: 0.0, dy: -self.insets.top)
}
}
open func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
}
open func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) {
if let extractedBackgroundNode = self.extractedBackgroundNode {
let transition: ContainedViewLayoutTransition = .animated(duration: duration, curve: animationCurve)
transition.animatePositionAdditive(node: extractedBackgroundNode, offset: CGPoint(x: -value.x, y: -value.y))
}
}
open func snapshotForReordering() -> UIView? {
return self.view.snapshotContentTree(keepTransform: true)
}
}
@@ -0,0 +1,29 @@
import Foundation
import UIKit
import AsyncDisplayKit
final class ListViewOverscrollBackgroundNode: ASDisplayNode {
private let backgroundNode: ASDisplayNode
var color: UIColor {
didSet {
self.backgroundNode.backgroundColor = color
}
}
init(color: UIColor) {
self.color = color
self.backgroundNode = ASDisplayNode()
self.backgroundNode.backgroundColor = color
self.backgroundNode.isLayerBacked = true
super.init()
self.addSubnode(self.backgroundNode)
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size))
}
}
@@ -0,0 +1,183 @@
import Foundation
import UIKit
import SwiftSignalKit
public final class ListViewReorderingGestureRecognizer: UIGestureRecognizer {
private let shouldBegin: (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, itemNode: ListViewItemNode?)
private let willBegin: (CGPoint) -> Void
private let began: (ListViewItemNode) -> Void
private let ended: () -> Void
private let moved: (CGFloat) -> Void
private var initialLocation: CGPoint?
private var longTapTimer: SwiftSignalKit.Timer?
private var longPressTimer: SwiftSignalKit.Timer?
private var itemNode: ListViewItemNode?
public init(shouldBegin: @escaping (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, itemNode: ListViewItemNode?), willBegin: @escaping (CGPoint) -> Void, began: @escaping (ListViewItemNode) -> Void, ended: @escaping () -> Void, moved: @escaping (CGFloat) -> Void) {
self.shouldBegin = shouldBegin
self.willBegin = willBegin
self.began = began
self.ended = ended
self.moved = moved
super.init(target: nil, action: nil)
}
deinit {
self.longTapTimer?.invalidate()
self.longPressTimer?.invalidate()
}
private func startLongTapTimer() {
self.longTapTimer?.invalidate()
let longTapTimer = SwiftSignalKit.Timer(timeout: 0.25, repeat: false, completion: { [weak self] in
self?.longTapTimerFired()
}, queue: Queue.mainQueue())
self.longTapTimer = longTapTimer
longTapTimer.start()
}
private func stopLongTapTimer() {
self.itemNode = nil
self.longTapTimer?.invalidate()
self.longTapTimer = nil
}
private func startLongPressTimer() {
self.longPressTimer?.invalidate()
let longPressTimer = SwiftSignalKit.Timer(timeout: 0.6, repeat: false, completion: { [weak self] in
self?.longPressTimerFired()
}, queue: Queue.mainQueue())
self.longPressTimer = longPressTimer
longPressTimer.start()
}
private func stopLongPressTimer() {
self.itemNode = nil
self.longPressTimer?.invalidate()
self.longPressTimer = nil
}
override public func reset() {
super.reset()
self.itemNode = nil
self.stopLongTapTimer()
self.stopLongPressTimer()
self.initialLocation = nil
}
private func longTapTimerFired() {
guard let location = self.initialLocation else {
return
}
self.longTapTimer?.invalidate()
self.longTapTimer = nil
self.willBegin(location)
}
private func longPressTimerFired() {
guard let _ = self.initialLocation else {
return
}
self.state = .began
self.longPressTimer?.invalidate()
self.longPressTimer = nil
self.longTapTimer?.invalidate()
self.longTapTimer = nil
if let itemNode = self.itemNode {
self.began(itemNode)
}
}
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
if self.numberOfTouches > 1 {
self.state = .failed
self.ended()
return
}
if self.state == .possible {
if let location = touches.first?.location(in: self.view) {
let (allowed, requiresLongPress, itemNode) = self.shouldBegin(location)
if allowed {
self.itemNode = itemNode
self.initialLocation = location
if requiresLongPress {
self.startLongTapTimer()
self.startLongPressTimer()
} else {
self.state = .began
if let itemNode = self.itemNode {
self.began(itemNode)
}
}
} else {
self.state = .failed
}
} else {
self.state = .failed
}
}
}
override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
self.initialLocation = nil
self.stopLongTapTimer()
if self.longPressTimer != nil {
self.stopLongPressTimer()
self.state = .failed
}
if self.state == .began || self.state == .changed {
self.ended()
self.state = .failed
}
}
override public func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
self.initialLocation = nil
self.stopLongTapTimer()
if self.longPressTimer != nil {
self.stopLongPressTimer()
self.state = .failed
}
if self.state == .began || self.state == .changed {
self.ended()
self.state = .failed
}
}
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) {
self.state = .changed
let offset = location.y - initialLocation.y
self.moved(offset)
} else if let touch = touches.first, let initialTapLocation = self.initialLocation, self.longPressTimer != nil {
let touchLocation = touch.location(in: self.view)
let dX = touchLocation.x - initialTapLocation.x
let dY = touchLocation.y - initialTapLocation.y
if dX * dX + dY * dY > 3.0 * 3.0 {
self.stopLongTapTimer()
self.stopLongPressTimer()
self.initialLocation = nil
self.state = .failed
}
}
}
}
@@ -0,0 +1,108 @@
import Foundation
import UIKit
import AsyncDisplayKit
private func generateShadowImage(mirror: Bool) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 45.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
if mirror {
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
}
context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 18.0, color: UIColor(white: 0.0, alpha: 0.35).cgColor)
context.setFillColor(UIColor(white: 0.0, alpha: 1.0).cgColor)
for _ in 0 ..< 1 {
context.fill(CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: 15.0)))
}
context.clear(CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: 15.0)))
})
}
private final class CopyView: UIView {
let topShadow: UIImageView
let bottomShadow: UIImageView
init(frame: CGRect, hasShadow: Bool) {
self.topShadow = UIImageView()
self.bottomShadow = UIImageView()
super.init(frame: frame)
if hasShadow {
self.topShadow.image = generateShadowImage(mirror: true)
self.bottomShadow.image = generateShadowImage(mirror: false)
}
self.addSubview(self.topShadow)
self.addSubview(self.bottomShadow)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
final class ListViewReorderingItemNode: ASDisplayNode {
weak var itemNode: ListViewItemNode?
var currentState: (Int, Int)?
private let copyView: CopyView
let initialLocation: CGPoint
init(itemNode: ListViewItemNode, initialLocation: CGPoint, hasShadow: Bool) {
self.itemNode = itemNode
self.copyView = CopyView(frame: CGRect(), hasShadow: hasShadow)
let snapshotView = itemNode.snapshotForReordering()
self.initialLocation = initialLocation
super.init()
if let snapshotView = snapshotView {
snapshotView.frame = CGRect(origin: CGPoint(), size: itemNode.bounds.size)
snapshotView.bounds.origin = itemNode.bounds.origin
self.copyView.addSubview(snapshotView)
}
self.view.addSubview(self.copyView)
self.copyView.frame = CGRect(origin: CGPoint(x: initialLocation.x, y: initialLocation.y), size: itemNode.bounds.size)
self.copyView.topShadow.frame = CGRect(origin: CGPoint(x: 0.0, y: -30.0), size: CGSize(width: copyView.bounds.size.width, height: 45.0))
self.copyView.bottomShadow.frame = CGRect(origin: CGPoint(x: 0.0, y: self.copyView.bounds.size.height - 15.0), size: CGSize(width: self.copyView.bounds.size.width, height: 45.0))
self.copyView.topShadow.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
self.copyView.bottomShadow.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
func updateOffset(offset: CGFloat) {
self.copyView.frame = CGRect(origin: CGPoint(x: initialLocation.x, y: initialLocation.y + offset), size: copyView.bounds.size)
}
func currentOffset() -> CGFloat? {
return self.copyView.center.y
}
func animateCompletion(completion: @escaping () -> Void) {
if let itemNode = self.itemNode {
let offset = itemNode.frame.midY - copyView.frame.midY
itemNode.isHidden = false
self.copyView.isHidden = true
itemNode.transitionOffset = offset
itemNode.addTransitionOffsetAnimation(0.0, duration: 0.3 * UIView.animationDurationFactor(), beginAt: CACurrentMediaTime())
completion()
/*itemNode.transitionOffset = 0.0
itemNode.setAnimationForKey("transitionOffset", animation: nil)
self.copyView.topShadow.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
self.copyView.bottomShadow.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
self.copyView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: itemNode.frame.midY - copyView.frame.midY), duration: 0.2, removeOnCompletion: false, additive: true, force: true, completion: { [weak itemNode] _ in
itemNode?.isHidden = false
completion()
})*/
} else {
completion()
}
}
}
@@ -0,0 +1,48 @@
import UIKit
public final class ListViewScroller: UIScrollView, UIGestureRecognizerDelegate {
override public init(frame: CGRect) {
super.init(frame: frame)
self.scrollsToTop = false
self.contentInsetAdjustmentBehavior = .never
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if otherGestureRecognizer is ListViewTapGestureRecognizer {
return true
}
return false
}
override public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is UIPanGestureRecognizer, let gestureRecognizers = gestureRecognizer.view?.gestureRecognizers {
for otherGestureRecognizer in gestureRecognizers {
if otherGestureRecognizer !== gestureRecognizer, let panGestureRecognizer = otherGestureRecognizer as? UIPanGestureRecognizer, panGestureRecognizer.minimumNumberOfTouches == 2 {
return gestureRecognizer.numberOfTouches < 2
}
}
if let view = gestureRecognizer.view?.hitTest(gestureRecognizer.location(in: gestureRecognizer.view), with: nil) as? UIControl {
return !view.isTracking
}
return true
} else {
return true
}
}
override public func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
var forceDecelerating = false
public override var isDecelerating: Bool {
return self.forceDecelerating || super.isDecelerating
}
}
@@ -0,0 +1,8 @@
import Foundation
import UIKit
public final class ListViewTapGestureRecognizer: UITapGestureRecognizer {
public func cancel() {
self.state = .failed
}
}
@@ -0,0 +1,4 @@
import Foundation
final class ListViewTempItemNode: ListViewItemNode {
}
@@ -0,0 +1,67 @@
import Foundation
import UIKit
import SwiftSignalKit
public typealias ListViewTransaction = (@escaping () -> Void) -> Void
public final class ListViewTransactionQueue {
private var transactions: [ListViewTransaction] = []
public final var transactionCompleted: () -> Void = { }
public init() {
}
public func addTransaction(_ transaction: @escaping ListViewTransaction) {
precondition(Thread.isMainThread)
let beginTransaction = self.transactions.count == 0
self.transactions.append(transaction)
if beginTransaction {
transaction({ [weak self] in
precondition(Thread.isMainThread)
if Thread.isMainThread {
if let strongSelf = self {
strongSelf.endTransaction()
}
} else {
Queue.mainQueue().async {
if let strongSelf = self {
strongSelf.endTransaction()
}
}
}
})
} else {
assert(true)
}
}
private func endTransaction() {
precondition(Thread.isMainThread)
Queue.mainQueue().async {
self.transactionCompleted()
if !self.transactions.isEmpty {
let _ = self.transactions.removeFirst()
}
if let nextTransaction = self.transactions.first {
nextTransaction({ [weak self] in
precondition(Thread.isMainThread)
if Thread.isMainThread {
if let strongSelf = self {
strongSelf.endTransaction()
}
} else {
Queue.mainQueue().async {
if let strongSelf = self {
strongSelf.endTransaction()
}
}
}
})
}
}
}
}
@@ -0,0 +1,482 @@
import Foundation
import UIKit
import SwiftSignalKit
private let orientationChangeDuration: Double = UIDevice.current.userInterfaceIdiom == .pad ? 0.4 : 0.3
private let defaultOrientations: UIInterfaceOrientationMask = {
if UIDevice.current.userInterfaceIdiom == .pad {
return .all
} else {
return .allButUpsideDown
}
}()
func getCurrentViewInterfaceOrientation(view: UIView) -> UIInterfaceOrientation {
var orientation: UIInterfaceOrientation = .portrait
if #available(iOS 13.0, *) {
if let window = view as? UIWindow {
if let windowScene = window.windowScene {
orientation = windowScene.interfaceOrientation
}
} else {
if let windowScene = view.window?.windowScene {
orientation = windowScene.interfaceOrientation
}
}
} else {
orientation = UIApplication.shared.statusBarOrientation
}
return orientation
}
public enum WindowUserInterfaceStyle {
case light
case dark
@available(iOS 12.0, *)
public init(style: UIUserInterfaceStyle) {
switch style {
case .light, .unspecified:
self = .light
case .dark:
self = .dark
@unknown default:
self = .dark
}
}
}
public final class PreviewingHostViewDelegate {
public let controllerForLocation: (UIView, CGPoint) -> (UIViewController, CGRect)?
public let commitController: (UIViewController) -> Void
public init(controllerForLocation: @escaping (UIView, CGPoint) -> (UIViewController, CGRect)?, commitController: @escaping (UIViewController) -> Void) {
self.controllerForLocation = controllerForLocation
self.commitController = commitController
}
}
public protocol PreviewingHostView {
@available(iOSApplicationExtension 9.0, iOS 9.0, *)
var previewingDelegate: PreviewingHostViewDelegate? { get }
}
private func tracePreviewingHostView(view: UIView, point: CGPoint) -> (UIView & PreviewingHostView, CGPoint)? {
if let view = view as? UIView & PreviewingHostView {
return (view, point)
}
if let superview = view.superview {
if let result = tracePreviewingHostView(view: superview, point: superview.convert(point, from: view)) {
return result
}
}
return nil
}
private final class WindowRootViewControllerView: UIView {
override var frame: CGRect {
get {
return super.frame
} set(value) {
var value = value
value.size.height += value.minY
value.origin.y = 0.0
super.frame = value
}
}
}
private final class WindowRootViewController: UIViewController, UIWindowSceneDelegate {
private var voiceOverStatusObserver: AnyObject?
private var registeredForPreviewing = false
var presentController: ((UIViewController, PresentationSurfaceLevel, Bool, (() -> Void)?) -> Void)?
var transitionToSize: ((CGSize, Double, UIInterfaceOrientation) -> Void)?
private var _systemUserInterfaceStyle = ValuePromise<WindowUserInterfaceStyle>(ignoreRepeated: true)
var systemUserInterfaceStyle: Signal<WindowUserInterfaceStyle, NoError> {
return self._systemUserInterfaceStyle.get()
}
var orientations: UIInterfaceOrientationMask = defaultOrientations {
didSet {
if oldValue != self.orientations {
if self.orientations == .portrait {
if #available(iOSApplicationExtension 16.0, iOS 16.0, *) {
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: .portrait))
self.setNeedsUpdateOfSupportedInterfaceOrientations()
} else if UIDevice.current.orientation != .portrait {
let value = UIInterfaceOrientation.portrait.rawValue
UIDevice.current.setValue(value, forKey: "orientation")
}
} else {
if #available(iOSApplicationExtension 16.0, iOS 16.0, *) {
self.setNeedsUpdateOfSupportedInterfaceOrientations()
} else {
UIViewController.attemptRotationToDeviceOrientation()
}
}
}
}
}
var gestureEdges: UIRectEdge = [] {
didSet {
if oldValue != self.gestureEdges {
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.setNeedsUpdateOfScreenEdgesDeferringSystemGestures()
}
}
}
}
var prefersOnScreenNavigationHidden: Bool = false {
didSet {
if oldValue != self.prefersOnScreenNavigationHidden {
self.setNeedsUpdateOfHomeIndicatorAutoHidden()
}
}
}
private var statusBarStyle: UIStatusBarStyle = .default
private var isStatusBarHidden: Bool = false
func updateStatusBar(style: UIStatusBarStyle, isHidden: Bool, transition: ContainedViewLayoutTransition) {
if self.statusBarStyle != style || self.isStatusBarHidden != isHidden {
self.statusBarStyle = style
self.isStatusBarHidden = isHidden
switch transition {
case .immediate:
self.setNeedsStatusBarAppearanceUpdate()
case .animated:
transition.animateView {
self.setNeedsStatusBarAppearanceUpdate()
}
}
}
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return self.statusBarStyle
}
override var prefersStatusBarHidden: Bool {
return self.isStatusBarHidden
}
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .fade
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return self.orientations
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
if #available(iOS 12.0, *) {
self._systemUserInterfaceStyle.set(WindowUserInterfaceStyle(style: self.traitCollection.userInterfaceStyle))
}
}
init() {
super.init(nibName: nil, bundle: nil)
self.extendedLayoutIncludesOpaqueBars = true
self.voiceOverStatusObserver = NotificationCenter.default.addObserver(forName: UIAccessibility.voiceOverStatusDidChangeNotification, object: nil, queue: OperationQueue.main, using: { _ in
})
if #available(iOS 13.0, *) {
self._systemUserInterfaceStyle.set(WindowUserInterfaceStyle(style: self.traitCollection.userInterfaceStyle))
} else {
self._systemUserInterfaceStyle.set(.light)
}
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
windowScene.delegate = self
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
if let voiceOverStatusObserver = self.voiceOverStatusObserver {
NotificationCenter.default.removeObserver(voiceOverStatusObserver)
}
}
@available(iOS 26.0, *)
func preferredWindowingControlStyle(for windowScene: UIWindowScene) -> UIWindowScene.WindowingControlStyle {
return .minimal
}
override var preferredScreenEdgesDeferringSystemGestures: UIRectEdge {
return self.gestureEdges
}
override var prefersHomeIndicatorAutoHidden: Bool {
return self.prefersOnScreenNavigationHidden
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
let orientation = getCurrentViewInterfaceOrientation(view: self.view)
UIView.performWithoutAnimation {
self.transitionToSize?(size, coordinator.transitionDuration, orientation)
}
}
override func loadView() {
self.view = WindowRootViewControllerView()
self.view.isOpaque = false
self.view.backgroundColor = nil
}
override public func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
super.present(viewControllerToPresent, animated: flag, completion: completion)
}
}
private final class NativeWindow: UIWindow, WindowHost {
var updateSize: ((CGSize) -> Void)?
var layoutSubviewsEvent: (() -> Void)?
var updateIsUpdatingOrientationLayout: ((Bool) -> Void)?
var updateToInterfaceOrientation: ((UIInterfaceOrientation) -> Void)?
var presentController: ((ContainableController, PresentationSurfaceLevel, Bool, @escaping () -> Void) -> Void)?
var presentControllerInGlobalOverlay: ((_ controller: ContainableController) -> Void)?
var addGlobalPortalHostViewImpl: ((PortalSourceView) -> Void)?
var hitTestImpl: ((CGPoint, UIEvent?) -> UIView?)?
var presentNativeImpl: ((UIViewController) -> Void)?
var invalidateDeferScreenEdgeGestureImpl: (() -> Void)?
var invalidatePrefersOnScreenNavigationHiddenImpl: (() -> Void)?
var invalidateSupportedOrientationsImpl: (() -> Void)?
var cancelInteractiveKeyboardGesturesImpl: (() -> Void)?
var forEachControllerImpl: (((ContainableController) -> Void) -> Void)?
var getAccessibilityElementsImpl: (() -> [Any]?)?
override var frame: CGRect {
get {
return super.frame
} set(value) {
let sizeUpdated = super.frame.size != value.size
var frameTransition: ContainedViewLayoutTransition = .immediate
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
let duration = UIView.inheritedAnimationDuration
if !duration.isZero {
frameTransition = .animated(duration: duration, curve: .easeInOut)
}
}
if sizeUpdated, case let .animated(duration, curve) = frameTransition {
let previousFrame = super.frame
super.frame = value
self.layer.animateFrame(from: previousFrame, to: value, duration: duration, timingFunction: curve.timingFunction)
} else {
super.frame = value
}
if sizeUpdated {
self.updateSize?(value.size)
}
}
}
override var bounds: CGRect {
get {
return super.bounds
}
set(value) {
let sizeUpdated = super.bounds.size != value.size
super.bounds = value
if sizeUpdated {
self.updateSize?(value.size)
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
if let gestureRecognizers = self.gestureRecognizers {
for recognizer in gestureRecognizers {
recognizer.delaysTouchesBegan = false
}
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
self.layoutSubviewsEvent?()
}
override func _update(toInterfaceOrientation arg1: Int32, duration arg2: Double, force arg3: Bool) {
self.updateIsUpdatingOrientationLayout?(true)
super._update(toInterfaceOrientation: arg1, duration: arg2, force: arg3)
self.updateIsUpdatingOrientationLayout?(false)
let orientation = UIInterfaceOrientation(rawValue: Int(arg1)) ?? .unknown
self.updateToInterfaceOrientation?(orientation)
}
func present(_ controller: ContainableController, on level: PresentationSurfaceLevel, blockInteraction: Bool, completion: @escaping () -> Void) {
self.presentController?(controller, level, blockInteraction, completion)
}
func presentInGlobalOverlay(_ controller: ContainableController) {
self.presentControllerInGlobalOverlay?(controller)
}
func addGlobalPortalHostView(sourceView: PortalSourceView) {
self.addGlobalPortalHostViewImpl?(sourceView)
}
func presentNative(_ controller: UIViewController) {
self.presentNativeImpl?(controller)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return self.hitTestImpl?(point, event)
}
func invalidateDeferScreenEdgeGestures() {
self.invalidateDeferScreenEdgeGestureImpl?()
}
func invalidatePrefersOnScreenNavigationHidden() {
self.invalidatePrefersOnScreenNavigationHiddenImpl?()
}
func invalidateSupportedOrientations() {
self.invalidateSupportedOrientationsImpl?()
}
func cancelInteractiveKeyboardGestures() {
self.cancelInteractiveKeyboardGesturesImpl?()
}
func forEachController(_ f: (ContainableController) -> Void) {
self.forEachControllerImpl?(f)
}
}
public func nativeWindowHostView() -> (UIWindow & WindowHost, WindowHostView) {
let window = NativeWindow(frame: UIScreen.main.bounds)
let rootViewController = WindowRootViewController()
window.rootViewController = rootViewController
rootViewController.viewWillAppear(false)
rootViewController.view.frame = CGRect(origin: CGPoint(), size: window.bounds.size)
rootViewController.viewDidAppear(false)
let hostView = WindowHostView(
containerView: rootViewController.view,
eventView: window,
isRotating: {
return window.isRotating()
},
systemUserInterfaceStyle: rootViewController.systemUserInterfaceStyle,
currentInterfaceOrientation: {
return getCurrentViewInterfaceOrientation(view: window)
},
updateSupportedInterfaceOrientations: { orientations in
rootViewController.orientations = orientations
},
updateDeferScreenEdgeGestures: { edges in
rootViewController.gestureEdges = edges
},
updatePrefersOnScreenNavigationHidden: { value in
rootViewController.prefersOnScreenNavigationHidden = value
},
updateStatusBar: { statusBarStyle, isStatusBarHidden, transition in
rootViewController.updateStatusBar(style: statusBarStyle, isHidden: isStatusBarHidden, transition: transition)
}
)
rootViewController.transitionToSize = { [weak hostView] size, duration, orientation in
hostView?.updateSize?(size, duration, orientation)
}
window.updateSize = { _ in
}
window.layoutSubviewsEvent = { [weak hostView] in
hostView?.layoutSubviews?()
}
window.updateIsUpdatingOrientationLayout = { [weak hostView] value in
hostView?.isUpdatingOrientationLayout = value
}
window.updateToInterfaceOrientation = { [weak hostView] orientation in
hostView?.updateToInterfaceOrientation?(orientation)
}
window.presentController = { [weak hostView] controller, level, blockInteraction, completion in
hostView?.present?(controller, level, blockInteraction, completion)
}
window.presentControllerInGlobalOverlay = { [weak hostView] controller in
hostView?.presentInGlobalOverlay?(controller)
}
window.addGlobalPortalHostViewImpl = { [weak hostView] sourceView in
hostView?.addGlobalPortalHostViewImpl?(sourceView)
}
window.presentNativeImpl = { [weak hostView] controller in
hostView?.presentNative?(controller)
}
hostView.nativeController = { [weak rootViewController] in
return rootViewController
}
window.hitTestImpl = { [weak hostView] point, event in
return hostView?.hitTest?(point, event)
}
window.invalidateDeferScreenEdgeGestureImpl = { [weak hostView] in
hostView?.invalidateDeferScreenEdgeGesture?()
}
window.invalidatePrefersOnScreenNavigationHiddenImpl = { [weak hostView] in
hostView?.invalidatePrefersOnScreenNavigationHidden?()
}
window.invalidateSupportedOrientationsImpl = { [weak hostView] in
hostView?.invalidateSupportedOrientations?()
}
window.cancelInteractiveKeyboardGesturesImpl = { [weak hostView] in
hostView?.cancelInteractiveKeyboardGestures?()
}
window.forEachControllerImpl = { [weak hostView] f in
hostView?.forEachController?(f)
}
window.getAccessibilityElementsImpl = { [weak hostView] in
return hostView?.getAccessibilityElements?()
}
rootViewController.presentController = { [weak hostView] controller, level, animated, completion in
if let hostView = hostView {
hostView.present?(LegacyPresentedController(legacyController: controller, presentation: .custom), level, false, completion ?? {})
completion?()
}
}
return (window, hostView)
}
@@ -0,0 +1,93 @@
import Foundation
import UIKit
import AsyncDisplayKit
public protocol MinimizedContainer: ASDisplayNode {
var navigationController: NavigationController? { get set }
var controllers: [MinimizableController] { get }
var isExpanded: Bool { get }
var willMaximize: ((MinimizedContainer) -> Void)? { get set }
var willDismiss: ((MinimizedContainer) -> Void)? { get set }
var didDismiss: ((MinimizedContainer) -> Void)? { get set }
var statusBarStyle: StatusBarStyle { get }
var statusBarStyleUpdated: (() -> Void)? { get set }
func addController(_ viewController: MinimizableController, topEdgeOffset: CGFloat?, beforeMaximize: @escaping (NavigationController, @escaping () -> Void) -> Void, transition: ContainedViewLayoutTransition)
func removeController(_ viewController: MinimizableController)
func maximizeController(_ viewController: MinimizableController, animated: Bool, completion: @escaping (Bool) -> Void)
func collapse()
func dismissAll(completion: @escaping () -> Void)
func updateLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition)
func collapsedHeight(layout: ContainerViewLayout) -> CGFloat
}
public protocol MinimizableController: ViewController {
var minimizedTopEdgeOffset: CGFloat? { get }
var minimizedBounds: CGRect? { get }
var isMinimized: Bool { get set }
var isMinimizable: Bool { get }
var minimizedIcon: UIImage? { get }
var minimizedProgress: Float? { get }
var isFullscreen: Bool { get }
func requestMinimize(topEdgeOffset: CGFloat?, initialVelocity: CGFloat?)
func makeContentSnapshotView() -> UIView?
func prepareContentSnapshotView()
func resetContentSnapshotView()
func shouldDismissImmediately() -> Bool
}
public extension MinimizableController {
var isFullscreen: Bool {
return false
}
var minimizedTopEdgeOffset: CGFloat? {
return nil
}
var minimizedBounds: CGRect? {
return nil
}
var isMinimized: Bool {
return false
}
var isMinimizable: Bool {
return false
}
var minimizedIcon: UIImage? {
return nil
}
var minimizedProgress: Float? {
return nil
}
func requestMinimize(topEdgeOffset: CGFloat?, initialVelocity: CGFloat?) {
}
func makeContentSnapshotView() -> UIView? {
return self.displayNode.view.snapshotView(afterScreenUpdates: false)
}
func prepareContentSnapshotView() {
}
func resetContentSnapshotView() {
}
func shouldDismissImmediately() -> Bool {
return true
}
}
@@ -0,0 +1,689 @@
import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
public final class NavigationContainer: ASDisplayNode, ASGestureRecognizerDelegate {
private final class Child {
let value: ViewController
var layout: ContainerViewLayout
init(value: ViewController, layout: ContainerViewLayout) {
self.value = value
self.layout = layout
}
}
private final class PendingChild {
enum TransitionType {
case push
case pop
}
let value: Child
let transitionType: TransitionType
let transition: ContainedViewLayoutTransition
let disposable: MetaDisposable = MetaDisposable()
var isReady: Bool = false
init(value: Child, transitionType: TransitionType, transition: ContainedViewLayoutTransition, update: @escaping (PendingChild) -> Void) {
self.value = value
self.transitionType = transitionType
self.transition = transition
var localIsReady: Bool?
self.disposable.set((value.value.ready.get()
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] _ in
if localIsReady == nil {
localIsReady = true
} else if let strongSelf = self {
update(strongSelf)
}
}))
if let _ = localIsReady {
self.isReady = true
} else {
localIsReady = false
}
}
deinit {
self.disposable.dispose()
}
}
private final class TopTransition {
let type: PendingChild.TransitionType
let previous: Child
let coordinator: NavigationTransitionCoordinator
init(type: PendingChild.TransitionType, previous: Child, coordinator: NavigationTransitionCoordinator) {
self.type = type
self.previous = previous
self.coordinator = coordinator
}
}
private struct State {
var layout: ContainerViewLayout?
var canBeClosed: Bool?
var top: Child?
var transition: TopTransition?
var pending: PendingChild?
}
private let isFlat: Bool
public private(set) var controllers: [ViewController] = []
private var state: State = State(layout: nil, canBeClosed: nil, top: nil, transition: nil, pending: nil)
weak var minimizedContainer: MinimizedContainer?
private var ignoreInputHeight: Bool = false
public private(set) var isReady: Bool = false
public var isReadyUpdated: (() -> Void)?
public var controllerRemoved: (ViewController) -> Void
public var requestFilterController: (ViewController) -> Void = { _ in }
public var keyboardViewManager: KeyboardViewManager? {
didSet {
}
}
public var canHaveKeyboardFocus: Bool = false {
didSet {
if self.canHaveKeyboardFocus != oldValue {
if !self.canHaveKeyboardFocus {
self.view.endEditing(true)
self.performUpdate(transition: .animated(duration: 0.5, curve: .spring))
}
}
}
}
public var isInFocus: Bool = false {
didSet {
if self.isInFocus != oldValue {
self.inFocusUpdated(isInFocus: self.isInFocus)
}
}
}
public func inFocusUpdated(isInFocus: Bool) {
self.state.top?.value.isInFocus = isInFocus
}
public var overflowInset: CGFloat = 0.0
private var currentKeyboardLeftEdge: CGFloat = 0.0
private var additionalKeyboardLeftEdgeOffset: CGFloat = 0.0
var statusBarStyle: StatusBarStyle = .Ignore {
didSet {
}
}
var statusBarStyleUpdated: ((ContainedViewLayoutTransition) -> Void)?
private var panRecognizer: InteractiveTransitionGestureRecognizer?
public init(isFlat: Bool, controllerRemoved: @escaping (ViewController) -> Void) {
self.isFlat = isFlat
self.controllerRemoved = controllerRemoved
super.init()
}
public override func didLoad() {
super.didLoad()
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] _ in
guard let strongSelf = self, strongSelf.controllers.count > 1 else {
return []
}
return .right
})
/*panRecognizer.dynamicEdgeWidth = { [weak self] _ in
guard let self, let controller = self.controllers.last, let value = controller.interactiveNavivationGestureEdgeWidth else {
return .constant(16.0)
}
return value
}*/
if #available(iOS 13.4, *) {
panRecognizer.allowedScrollTypesMask = .continuous
}
panRecognizer.delegate = self.wrappedGestureRecognizerDelegate
panRecognizer.delaysTouchesBegan = false
panRecognizer.cancelsTouchesInView = true
self.panRecognizer = panRecognizer
self.view.addGestureRecognizer(panRecognizer)
/*self.view.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in
guard let strongSelf = self else {
return false
}
if strongSelf.state.transition != nil {
return true
}
return false
}*/
}
func hasNonReadyControllers() -> Bool {
if let pending = self.state.pending, !pending.isReady {
return true
}
return false
}
public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer == self.panRecognizer, let gestureRecognizer = self.panRecognizer, gestureRecognizer.numberOfTouches == 0 {
let translation = gestureRecognizer.velocity(in: gestureRecognizer.view)
if abs(translation.y) > 4.0 && abs(translation.y) > abs(translation.x) * 2.5 {
return false
}
if translation.x < 4.0 {
return false
}
if self.controllers.count == 1 {
return false
}
return true
} else {
return true
}
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer {
return false
}
if let _ = otherGestureRecognizer as? UIPanGestureRecognizer {
return true
}
return false
}
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
guard let layout = self.state.layout else {
return
}
guard self.state.transition == nil else {
return
}
let beginGesture = self.controllers.count > 1
if beginGesture {
let topController = self.controllers[self.controllers.count - 1]
let bottomController = self.controllers[self.controllers.count - 2]
if !topController.attemptNavigation({ [weak self, weak topController] in
if let self, let topController {
self.requestFilterController(topController)
}
}) {
return
}
topController.viewWillDisappear(true)
let topNode = topController.displayNode
var bottomControllerLayout = layout
if bottomController.view.disableAutomaticKeyboardHandling.isEmpty {
if let minimizedContainer = self.minimizedContainer, (bottomControllerLayout.inputHeight ?? 0.0) > 0.0 {
var updatedSize = bottomControllerLayout.size
var updatedIntrinsicInsets = bottomControllerLayout.intrinsicInsets
updatedSize.height -= minimizedContainer.collapsedHeight(layout: layout)
updatedIntrinsicInsets.bottom = 0.0
bottomControllerLayout = bottomControllerLayout.withUpdatedSize(updatedSize).withUpdatedIntrinsicInsets(updatedIntrinsicInsets)
}
bottomControllerLayout = bottomControllerLayout.withUpdatedInputHeight(nil)
}
bottomController.containerLayoutUpdated(bottomControllerLayout, transition: .immediate)
bottomController.viewWillAppear(true)
let bottomNode = bottomController.displayNode
let screenCornerRadius = self.minimizedContainer == nil && self.state.canBeClosed != true ? layout.deviceMetrics.screenCornerRadius : 0.0
let navigationTransitionCoordinator = NavigationTransitionCoordinator(transition: .Pop, isInteractive: true, isFlat: self.isFlat, container: self, topNode: topNode, topNavigationBar: topController.transitionNavigationBar, bottomNode: bottomNode, bottomNavigationBar: bottomController.transitionNavigationBar, screenCornerRadius: screenCornerRadius, didUpdateProgress: { [weak self, weak bottomController] progress, transition, topFrame, bottomFrame in
if let strongSelf = self {
if let top = strongSelf.state.top {
strongSelf.syncKeyboard(leftEdge: top.value.displayNode.frame.minX, transition: transition)
var updatedStatusBarStyle = strongSelf.statusBarStyle
if let bottomController = bottomController, progress >= 0.3 {
updatedStatusBarStyle = bottomController.statusBar.statusBarStyle
} else {
updatedStatusBarStyle = top.value.statusBar.statusBarStyle
}
if strongSelf.statusBarStyle != updatedStatusBarStyle {
strongSelf.statusBarStyle = updatedStatusBarStyle
strongSelf.statusBarStyleUpdated?(.animated(duration: 0.3, curve: .easeInOut))
}
}
}
})
bottomController.displayNode.recursivelyEnsureDisplaySynchronously(true)
self.state.transition = TopTransition(type: .pop, previous: Child(value: bottomController, layout: layout), coordinator: navigationTransitionCoordinator)
}
case .changed:
if let navigationTransitionCoordinator = self.state.transition?.coordinator, !navigationTransitionCoordinator.animatingCompletion {
let translation = recognizer.translation(in: self.view).x
let progress = max(0.0, min(1.0, translation / self.view.frame.width))
navigationTransitionCoordinator.updateProgress(progress, transition: .immediate, completion: {})
}
case .ended, .cancelled:
if let navigationTransitionCoordinator = self.state.transition?.coordinator, !navigationTransitionCoordinator.animatingCompletion {
let velocity = recognizer.velocity(in: self.view).x
if velocity > 1000 || navigationTransitionCoordinator.progress > 0.2 {
self.state.top?.value.viewWillLeaveNavigation()
navigationTransitionCoordinator.animateCompletion(velocity, completion: { [weak self] in
guard let strongSelf = self, let _ = strongSelf.state.layout, let _ = strongSelf.state.transition, let top = strongSelf.state.top else {
return
}
let topController = top.value
if viewTreeContainsFirstResponder(view: top.value.view) {
strongSelf.ignoreInputHeight = true
}
strongSelf.keyboardViewManager?.dismissEditingWithoutAnimation(view: topController.view)
strongSelf.state.transition = nil
strongSelf.controllerRemoved(top.value)
strongSelf.ignoreInputHeight = false
})
} else {
navigationTransitionCoordinator.animateCancel({ [weak self] in
guard let strongSelf = self, let top = strongSelf.state.top, let transition = strongSelf.state.transition else {
return
}
strongSelf.state.transition = nil
top.value.viewDidAppear(true)
transition.previous.value.viewDidDisappear(true)
})
}
}
default:
break
}
}
public func update(layout: ContainerViewLayout, canBeClosed: Bool, controllers: [ViewController], transition: ContainedViewLayoutTransition) {
self.state.layout = layout
self.state.canBeClosed = canBeClosed
var controllersUpdated = false
if self.controllers.count != controllers.count {
controllersUpdated = true
} else {
for i in 0 ..< controllers.count {
if self.controllers[i] !== controllers[i] {
controllersUpdated = true
break
}
}
}
if controllersUpdated {
let previousControllers = self.controllers
self.controllers = controllers
for i in 0 ..< controllers.count {
if i == 0 {
if canBeClosed {
controllers[i].transitionNavigationBar?.previousItem = .close
controllers[i].previousItem = .close
} else {
controllers[i].transitionNavigationBar?.previousItem = nil
controllers[i].previousItem = nil
}
} else {
controllers[i].transitionNavigationBar?.previousItem = .item(controllers[i - 1].navigationItem)
controllers[i].previousItem = .item(controllers[i - 1].navigationItem)
}
}
if controllers.last !== self.state.top?.value {
self.state.top?.value.statusBar.alphaUpdated = nil
if let controller = controllers.last {
controller.statusBar.alphaUpdated = { [weak self, weak controller] transition in
guard let strongSelf = self, let controller = controller else {
return
}
if strongSelf.state.top?.value === controller && strongSelf.state.transition == nil {
strongSelf.statusBarStyleUpdated?(transition)
}
}
}
if controllers.last !== self.state.pending?.value.value {
self.state.pending = nil
if let last = controllers.last {
let transitionType: PendingChild.TransitionType
if !previousControllers.contains(where: { $0 === last }) {
transitionType = .push
} else {
transitionType = .pop
}
var updatedLayout = layout
if last.view.disableAutomaticKeyboardHandling.isEmpty {
updatedLayout = updatedLayout.withUpdatedInputHeight(nil)
}
self.state.pending = PendingChild(value: self.makeChild(layout: updatedLayout, value: last), transitionType: transitionType, transition: transition, update: { [weak self] pendingChild in
self?.pendingChildIsReady(pendingChild)
})
}
}
}
}
var statusBarTransition = transition
var ignoreInputHeight = false
if let pending = self.state.pending {
if pending.isReady {
self.state.pending = nil
let previous = self.state.top
previous?.value.isInFocus = false
self.state.top = pending.value
var updatedLayout = layout
if pending.value.value.view.disableAutomaticKeyboardHandling.isEmpty {
updatedLayout = updatedLayout.withUpdatedInputHeight(nil)
}
if case .regular = layout.metrics.widthClass, pending.value.layout.inputHeight == nil {
ignoreInputHeight = true
}
self.topTransition(from: previous, to: pending.value, transitionType: pending.transitionType, layout: updatedLayout, transition: pending.transition)
self.state.top?.value.isInFocus = self.isInFocus
statusBarTransition = pending.transition
if !self.isReady {
self.isReady = true
self.isReadyUpdated?()
}
}
}
if controllers.isEmpty && self.state.top != nil {
let previous = self.state.top
previous?.value.isInFocus = false
self.state.top = nil
self.topTransition(from: previous, to: nil, transitionType: .pop, layout: layout, transition: .immediate)
}
var updatedStatusBarStyle = self.statusBarStyle
if let top = self.state.top {
var updatedLayout = layout
if let _ = self.state.transition, top.value.view.disableAutomaticKeyboardHandling.isEmpty {
if !viewTreeContainsFirstResponder(view: top.value.view) {
updatedLayout = updatedLayout.withUpdatedInputHeight(nil)
}
}
if ignoreInputHeight {
updatedLayout = updatedLayout.withUpdatedInputHeight(nil)
}
self.applyLayout(layout: updatedLayout, to: top, isMaster: true, transition: transition)
if let childTransition = self.state.transition, childTransition.coordinator.isInteractive {
switch childTransition.type {
case .push:
if childTransition.coordinator.progress >= 0.3 {
updatedStatusBarStyle = top.value.statusBar.statusBarStyle
} else {
updatedStatusBarStyle = childTransition.previous.value.statusBar.statusBarStyle
}
case .pop:
if childTransition.coordinator.progress >= 0.3 {
updatedStatusBarStyle = childTransition.previous.value.statusBar.statusBarStyle
} else {
updatedStatusBarStyle = top.value.statusBar.statusBarStyle
}
}
} else {
updatedStatusBarStyle = top.value.statusBar.statusBarStyle
}
} else {
updatedStatusBarStyle = .Ignore
}
if self.statusBarStyle != updatedStatusBarStyle {
self.statusBarStyle = updatedStatusBarStyle
self.statusBarStyleUpdated?(statusBarTransition)
}
}
public var shouldAnimateDisappearance: Bool = false
private func topTransition(from fromValue: Child?, to toValue: Child?, transitionType: PendingChild.TransitionType, layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
if case .animated = transition, let fromValue = fromValue, let toValue = toValue {
if let currentTransition = self.state.transition {
currentTransition.coordinator.performCompletion(completion: {
})
}
fromValue.value.viewWillLeaveNavigation()
fromValue.value.viewWillDisappear(true)
toValue.value.viewWillAppear(true)
toValue.value.setIgnoreAppearanceMethodInvocations(true)
if let layout = self.state.layout {
toValue.value.displayNode.frame = CGRect(origin: CGPoint(), size: layout.size)
}
let mappedTransitionType: NavigationTransition
let topController: ViewController
let bottomController: ViewController
switch transitionType {
case .push:
mappedTransitionType = .Push
self.addSubnode(toValue.value.displayNode)
topController = toValue.value
bottomController = fromValue.value
case .pop:
mappedTransitionType = .Pop
self.insertSubnode(toValue.value.displayNode, belowSubnode: fromValue.value.displayNode)
topController = fromValue.value
bottomController = toValue.value
}
toValue.value.setIgnoreAppearanceMethodInvocations(false)
let screenCornerRadius = self.minimizedContainer == nil && self.state.canBeClosed != true ? layout.deviceMetrics.screenCornerRadius : 0.0
let topTransition = TopTransition(type: transitionType, previous: fromValue, coordinator: NavigationTransitionCoordinator(transition: mappedTransitionType, isInteractive: false, isFlat: self.isFlat, container: self, topNode: topController.displayNode, topNavigationBar: topController.transitionNavigationBar, bottomNode: bottomController.displayNode, bottomNavigationBar: bottomController.transitionNavigationBar, screenCornerRadius: screenCornerRadius, didUpdateProgress: { [weak self] _, transition, topFrame, bottomFrame in
guard let strongSelf = self else {
return
}
switch transitionType {
case .push:
if let _ = strongSelf.state.transition, let top = strongSelf.state.top, viewTreeContainsFirstResponder(view: top.value.view) {
strongSelf.syncKeyboard(leftEdge: topFrame.minX, transition: transition)
} else {
if let hasActiveInput = strongSelf.state.top?.value.hasActiveInput, hasActiveInput {
} else {
strongSelf.syncKeyboard(leftEdge: topFrame.minX - bottomFrame.width, transition: transition)
}
}
case .pop:
strongSelf.syncKeyboard(leftEdge: topFrame.minX, transition: transition)
}
}))
self.state.transition = topTransition
topTransition.coordinator.animateCompletion(0.0, completion: { [weak self, weak topTransition] in
guard let strongSelf = self, let topTransition = topTransition, strongSelf.state.transition === topTransition else {
return
}
if viewTreeContainsFirstResponder(view: topTransition.previous.value.view) {
strongSelf.ignoreInputHeight = true
}
strongSelf.keyboardViewManager?.dismissEditingWithoutAnimation(view: topTransition.previous.value.view)
strongSelf.state.transition = nil
if strongSelf.shouldAnimateDisappearance {
let displayNode = topTransition.previous.value.displayNode
displayNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak displayNode] _ in
displayNode?.removeFromSupernode()
displayNode?.layer.removeAllAnimations()
})
} else {
topTransition.previous.value.setIgnoreAppearanceMethodInvocations(true)
topTransition.previous.value.displayNode.removeFromSupernode()
topTransition.previous.value.setIgnoreAppearanceMethodInvocations(false)
}
topTransition.previous.value.viewDidDisappear(true)
if let toValue = strongSelf.state.top, let layout = strongSelf.state.layout {
toValue.value.displayNode.frame = CGRect(origin: CGPoint(), size: layout.size)
strongSelf.applyLayout(layout: layout, to: toValue, isMaster: true, transition: .immediate)
toValue.value.viewDidAppear(true)
}
strongSelf.ignoreInputHeight = false
})
} else {
if let fromValue = fromValue {
if viewTreeContainsFirstResponder(view: fromValue.value.view) {
self.ignoreInputHeight = true
}
fromValue.value.viewWillLeaveNavigation()
fromValue.value.viewWillDisappear(false)
self.keyboardViewManager?.dismissEditingWithoutAnimation(view: fromValue.value.view)
fromValue.value.setIgnoreAppearanceMethodInvocations(true)
fromValue.value.displayNode.removeFromSupernode()
fromValue.value.setIgnoreAppearanceMethodInvocations(false)
fromValue.value.viewDidDisappear(false)
}
if let toValue = toValue {
self.applyLayout(layout: layout, to: toValue, isMaster: true, transition: .immediate)
toValue.value.displayNode.frame = CGRect(origin: CGPoint(), size: layout.size)
toValue.value.viewWillAppear(false)
toValue.value.setIgnoreAppearanceMethodInvocations(true)
self.addSubnode(toValue.value.displayNode)
toValue.value.setIgnoreAppearanceMethodInvocations(false)
toValue.value.displayNode.recursivelyEnsureDisplaySynchronously(true)
toValue.value.viewDidAppear(false)
}
self.ignoreInputHeight = false
}
}
private func makeChild(layout: ContainerViewLayout, value: ViewController) -> Child {
var updatedLayout = layout
if value.view.disableAutomaticKeyboardHandling.isEmpty {
updatedLayout = updatedLayout.withUpdatedInputHeight(nil)
}
value.containerLayoutUpdated(updatedLayout, transition: .immediate)
return Child(value: value, layout: updatedLayout)
}
private func applyLayout(layout: ContainerViewLayout, to child: Child, isMaster: Bool, transition: ContainedViewLayoutTransition) {
var childFrame = CGRect(origin: CGPoint(), size: layout.size)
var updatedLayout = layout
var shouldSyncKeyboard = false
if let transition = self.state.transition {
childFrame.origin.x = child.value.displayNode.frame.origin.x
switch transition.type {
case .pop:
if transition.previous.value === child.value {
shouldSyncKeyboard = true
}
case .push:
break
}
if updatedLayout.inputHeight != nil {
if !self.canHaveKeyboardFocus && child.value.view.disableAutomaticKeyboardHandling.isEmpty {
updatedLayout = updatedLayout.withUpdatedInputHeight(nil)
}
}
} else {
if isMaster {
shouldSyncKeyboard = true
}
if updatedLayout.inputHeight != nil && child.value.view.disableAutomaticKeyboardHandling.isEmpty {
if !self.canHaveKeyboardFocus || self.ignoreInputHeight {
updatedLayout = updatedLayout.withUpdatedInputHeight(nil)
}
}
}
if child.value.displayNode.frame != childFrame {
transition.updateFrame(node: child.value.displayNode, frame: childFrame)
}
if shouldSyncKeyboard && isMaster {
self.syncKeyboard(leftEdge: childFrame.minX, transition: transition)
}
if child.layout != updatedLayout {
child.layout = updatedLayout
child.value.containerLayoutUpdated(updatedLayout, transition: transition)
}
}
public func updateAdditionalKeyboardLeftEdgeOffset(_ offset: CGFloat, transition: ContainedViewLayoutTransition) {
self.additionalKeyboardLeftEdgeOffset = offset
self.syncKeyboard(leftEdge: self.currentKeyboardLeftEdge, transition: transition)
}
private func syncKeyboard(leftEdge: CGFloat, transition: ContainedViewLayoutTransition) {
self.currentKeyboardLeftEdge = leftEdge
self.keyboardViewManager?.update(leftEdge: self.additionalKeyboardLeftEdgeOffset + leftEdge, transition: transition)
}
private func pendingChildIsReady(_ child: PendingChild) {
if let pending = self.state.pending, pending === child {
pending.isReady = true
self.performUpdate(transition: .immediate)
}
}
private func performUpdate(transition: ContainedViewLayoutTransition) {
if let layout = self.state.layout, let canBeClosed = self.state.canBeClosed {
self.update(layout: layout, canBeClosed: canBeClosed, controllers: self.controllers, transition: transition)
}
}
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.bounds.contains(point) {
return nil
}
if self.state.transition != nil {
return self.view
}
return super.hitTest(point, with: event)
}
func combinedSupportedOrientations(currentOrientationToLock: UIInterfaceOrientationMask) -> ViewControllerSupportedOrientations {
var supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .allButUpsideDown)
if let controller = self.controllers.last {
if controller.lockOrientation {
if let lockedOrientation = controller.lockedOrientation {
supportedOrientations = supportedOrientations.intersection(ViewControllerSupportedOrientations(regularSize: lockedOrientation, compactSize: lockedOrientation))
} else {
supportedOrientations = supportedOrientations.intersection(ViewControllerSupportedOrientations(regularSize: currentOrientationToLock, compactSize: currentOrientationToLock))
}
} else {
supportedOrientations = supportedOrientations.intersection(controller.supportedOrientations)
}
}
if let transition = self.state.transition {
let controller = transition.previous.value
if controller.lockOrientation {
if let lockedOrientation = controller.lockedOrientation {
supportedOrientations = supportedOrientations.intersection(ViewControllerSupportedOrientations(regularSize: lockedOrientation, compactSize: lockedOrientation))
} else {
supportedOrientations = supportedOrientations.intersection(ViewControllerSupportedOrientations(regularSize: currentOrientationToLock, compactSize: currentOrientationToLock))
}
} else {
supportedOrientations = supportedOrientations.intersection(controller.supportedOrientations)
}
}
return supportedOrientations
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,121 @@
import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
enum RootNavigationLayout {
case split([ViewController], [ViewController])
case flat([ViewController])
}
struct ModalContainerLayout {
var controllers: [ViewController]
var isFlat: Bool
var flatReceivesModalTransition: Bool
var isStandalone: Bool
}
struct NavigationLayout {
var root: RootNavigationLayout
var modal: [ModalContainerLayout]
}
func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewLayout, controllers: [ViewController]) -> NavigationLayout {
var rootControllers: [ViewController] = []
var modalStack: [ModalContainerLayout] = []
for controller in controllers {
let requiresModal: Bool
var beginsModal: Bool = false
var isFlat: Bool = false
var flatReceivesModalTransition: Bool = false
var isStandalone: Bool = false
switch controller.navigationPresentation {
case .default:
requiresModal = false
case .master:
requiresModal = false
case .modal:
requiresModal = true
beginsModal = true
case .flatModal:
requiresModal = true
beginsModal = true
isFlat = true
flatReceivesModalTransition = controller.flatReceivesModalTransition
case .standaloneModal:
requiresModal = true
beginsModal = true
isStandalone = true
case .standaloneFlatModal:
requiresModal = true
beginsModal = true
isStandalone = true
isFlat = true
case .modalInLargeLayout:
switch layout.metrics.widthClass {
case .compact:
requiresModal = false
case .regular:
requiresModal = true
}
case .modalInCompactLayout:
switch layout.metrics.widthClass {
case .compact:
requiresModal = true
case .regular:
requiresModal = true
beginsModal = true
isFlat = true
}
}
if requiresModal {
controller._presentedInModal = true
if beginsModal || modalStack.isEmpty || modalStack[modalStack.count - 1].isStandalone {
modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, flatReceivesModalTransition: flatReceivesModalTransition, isStandalone: isStandalone))
} else {
modalStack[modalStack.count - 1].controllers.append(controller)
}
} else if !modalStack.isEmpty {
if modalStack[modalStack.count - 1].isFlat {
} else {
controller._presentedInModal = true
}
if modalStack[modalStack.count - 1].isStandalone {
modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, flatReceivesModalTransition: flatReceivesModalTransition, isStandalone: isStandalone))
} else {
modalStack[modalStack.count - 1].controllers.append(controller)
}
} else {
controller._presentedInModal = false
rootControllers.append(controller)
}
}
let rootLayout: RootNavigationLayout
switch mode {
case .single:
rootLayout = .flat(rootControllers)
case .automaticMasterDetail:
switch layout.metrics.widthClass {
case .compact:
rootLayout = .flat(rootControllers)
case .regular:
let masterControllers = rootControllers.filter {
if case .master = $0.navigationPresentation {
return true
} else {
return false
}
}
let detailControllers = rootControllers.filter {
if case .master = $0.navigationPresentation {
return false
} else {
return true
}
}
rootLayout = .split(masterControllers, detailControllers)
}
}
return NavigationLayout(root: rootLayout, modal: modalStack)
}
@@ -0,0 +1,610 @@
import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
import UIKitRuntimeUtils
final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDelegate {
private var theme: NavigationControllerTheme
let isFlat: Bool
private let dim: ASDisplayNode
private let scrollNode: ASScrollNode
let container: NavigationContainer
private var panRecognizer: InteractiveTransitionGestureRecognizer?
private(set) var isReady: Bool = false
private(set) var dismissProgress: CGFloat = 0.0
var isReadyUpdated: (() -> Void)?
var updateDismissProgress: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
var interactivelyDismissed: ((Bool) -> Void)?
private var isUpdatingState = false
private var ignoreScrolling = false
private var isDismissed = false
private var isInteractiveDimissEnabled = true
private var validLayout: ContainerViewLayout?
private var horizontalDismissOffset: CGFloat?
var keyboardViewManager: KeyboardViewManager? {
didSet {
if self.keyboardViewManager !== oldValue {
self.container.keyboardViewManager = self.keyboardViewManager
}
}
}
var canHaveKeyboardFocus: Bool = false {
didSet {
self.container.canHaveKeyboardFocus = self.canHaveKeyboardFocus
}
}
init(theme: NavigationControllerTheme, isFlat: Bool, controllerRemoved: @escaping (ViewController) -> Void) {
self.theme = theme
self.isFlat = isFlat
self.dim = ASDisplayNode()
self.dim.alpha = 0.0
self.scrollNode = ASScrollNode()
self.container = NavigationContainer(isFlat: false, controllerRemoved: controllerRemoved)
self.container.clipsToBounds = true
super.init()
self.addSubnode(self.dim)
self.addSubnode(self.scrollNode)
self.scrollNode.addSubnode(self.container)
self.isReady = self.container.isReady
self.container.isReadyUpdated = { [weak self] in
guard let strongSelf = self else {
return
}
if !strongSelf.isReady {
strongSelf.isReady = true
if !strongSelf.isUpdatingState {
strongSelf.isReadyUpdated?()
}
}
}
applySmoothRoundedCorners(self.container.layer)
}
override func didLoad() {
super.didLoad()
self.scrollNode.view.alwaysBounceVertical = false
self.scrollNode.view.alwaysBounceHorizontal = false
self.scrollNode.view.bounces = false
self.scrollNode.view.showsVerticalScrollIndicator = false
self.scrollNode.view.showsHorizontalScrollIndicator = false
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
}
self.scrollNode.view.delaysContentTouches = false
self.scrollNode.view.clipsToBounds = false
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
self.scrollNode.view.tag = 0x5C4011
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] _ in
guard let strongSelf = self, !strongSelf.isDismissed else {
return []
}
return .right
})
if #available(iOS 13.4, *) {
panRecognizer.allowedScrollTypesMask = .continuous
}
self.panRecognizer = panRecognizer
if let layout = self.validLayout {
switch layout.metrics.widthClass {
case .compact:
panRecognizer.isEnabled = true
case .regular:
panRecognizer.isEnabled = false
}
}
panRecognizer.delegate = self.wrappedGestureRecognizerDelegate
panRecognizer.delaysTouchesBegan = false
panRecognizer.cancelsTouchesInView = true
if !self.isFlat {
self.view.addGestureRecognizer(panRecognizer)
self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
}
}
public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer == self.panRecognizer, let gestureRecognizer = self.panRecognizer, gestureRecognizer.numberOfTouches == 0 {
let translation = gestureRecognizer.velocity(in: gestureRecognizer.view)
if abs(translation.y) > 4.0 && abs(translation.y) > abs(translation.x) * 2.5 {
return false
}
if translation.x < 4.0 {
return false
}
if self.isDismissed {
return false
}
return true
} else {
return true
}
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if let _ = otherGestureRecognizer as? UIPanGestureRecognizer {
return true
}
return false
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer {
return true
}
return false
}
private func checkInteractiveDismissWithControllers() -> Bool {
if let controller = self.container.controllers.last {
if !controller.attemptNavigation({
}) {
return false
}
}
return true
}
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
self.horizontalDismissOffset = 0.0
case .changed:
let translation = max(0.0, recognizer.translation(in: self.view).x)
let progress = translation / self.bounds.width
self.horizontalDismissOffset = translation
self.dismissProgress = progress
self.applyDismissProgress(transition: .immediate, completion: {})
self.container.updateAdditionalKeyboardLeftEdgeOffset(translation, transition: .immediate)
case .ended, .cancelled:
let translation = max(0.0, recognizer.translation(in: self.view).x)
let progress = translation / self.bounds.width
let velocity = recognizer.velocity(in: self.view).x
if (velocity > 1000 || progress > 0.2) && self.checkInteractiveDismissWithControllers() {
self.isDismissed = true
self.horizontalDismissOffset = self.bounds.width
self.dismissProgress = 1.0
let transition: ContainedViewLayoutTransition = .animated(duration: 0.5, curve: .spring)
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(x: self.bounds.width, y: 0.0), size: self.scrollNode.bounds.size))
self.container.updateAdditionalKeyboardLeftEdgeOffset(self.bounds.width, transition: transition)
self.applyDismissProgress(transition: transition, completion: { [weak self] in
guard let strongSelf = self else {
return
}
let hadInputFocus = viewTreeContainsFirstResponder(view: strongSelf.view)
strongSelf.keyboardViewManager?.dismissEditingWithoutAnimation(view: strongSelf.view)
strongSelf.interactivelyDismissed?(hadInputFocus)
})
} else {
self.horizontalDismissOffset = nil
self.dismissProgress = 0.0
let transition: ContainedViewLayoutTransition = .animated(duration: 0.1, curve: .easeInOut)
self.applyDismissProgress(transition: transition, completion: {})
self.container.updateAdditionalKeyboardLeftEdgeOffset(0.0, transition: transition)
}
default:
break
}
}
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
if !self.isDismissed {
self.dismissWithAnimation()
}
}
}
private func dismissWithAnimation() {
let scrollView = self.scrollNode.view
let targetOffset: CGFloat
let duration = 0.3
let transition: ContainedViewLayoutTransition
let dismissProgress: CGFloat
dismissProgress = 1.0
targetOffset = 0.0
transition = .animated(duration: duration, curve: .easeInOut)
self.isDismissed = true
self.ignoreScrolling = true
let deltaY = targetOffset - scrollView.contentOffset.y
scrollView.setContentOffset(scrollView.contentOffset, animated: false)
scrollView.setContentOffset(CGPoint(x: 0.0, y: targetOffset), animated: false)
transition.animateOffsetAdditive(layer: self.scrollNode.layer, offset: -deltaY, completion: { [weak self] in
guard let strongSelf = self else {
return
}
if targetOffset == 0.0 {
strongSelf.interactivelyDismissed?(false)
}
})
self.ignoreScrolling = false
self.dismissProgress = dismissProgress
self.applyDismissProgress(transition: transition, completion: {})
self.view.endEditing(true)
}
private var isDraggingHeader = false
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if self.ignoreScrolling || self.isDismissed {
return
}
var progress = (self.bounds.height - scrollView.bounds.origin.y) / self.bounds.height
progress = max(0.0, min(1.0, progress))
self.dismissProgress = progress
self.applyDismissProgress(transition: .immediate, completion: {})
let location = scrollView.panGestureRecognizer.location(in: scrollView).offsetBy(dx: 0.0, dy: -self.container.frame.minY)
self.isDraggingHeader = location.y < 66.0
}
private func applyDismissProgress(transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
transition.updateAlpha(node: self.dim, alpha: 1.0 - self.dismissProgress, completion: { _ in
completion()
})
self.updateDismissProgress?(self.dismissProgress, transition)
}
private var endDraggingVelocity: CGPoint?
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
let velocity = self.endDraggingVelocity ?? CGPoint()
self.endDraggingVelocity = nil
var progress = (self.bounds.height - scrollView.bounds.origin.y) / self.bounds.height
progress = max(0.0, min(1.0, progress))
let targetOffset: CGFloat
let velocityFactor: CGFloat = 0.4 / max(1.0, abs(velocity.y))
let duration = Double(min(0.3, velocityFactor))
let transition: ContainedViewLayoutTransition
let dismissProgress: CGFloat
if (velocity.y < -0.5 || progress >= 0.5) && self.checkInteractiveDismissWithControllers() {
if let controller = self.container.controllers.last as? MinimizableController, controller.isMinimizable {
dismissProgress = 0.0
targetOffset = 0.0
transition = .immediate
let topEdgeOffset = self.container.view.convert(self.container.bounds, to: self.view).minY
controller.requestMinimize(topEdgeOffset: topEdgeOffset, initialVelocity: velocity.y)
self.dim.removeFromSupernode()
} else {
dismissProgress = 1.0
targetOffset = 0.0
transition = .animated(duration: duration, curve: .easeInOut)
self.isDismissed = true
}
} else {
dismissProgress = 0.0
targetOffset = self.bounds.height
transition = .animated(duration: 0.5, curve: .spring)
}
self.ignoreScrolling = true
let deltaY = targetOffset - scrollView.contentOffset.y
scrollView.setContentOffset(scrollView.contentOffset, animated: false)
scrollView.setContentOffset(CGPoint(x: 0.0, y: targetOffset), animated: false)
transition.animateOffsetAdditive(layer: self.scrollNode.layer, offset: -deltaY, completion: { [weak self] in
guard let strongSelf = self else {
return
}
if targetOffset == 0.0 {
strongSelf.interactivelyDismissed?(false)
}
})
self.ignoreScrolling = false
self.dismissProgress = dismissProgress
self.applyDismissProgress(transition: transition, completion: {})
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
self.endDraggingVelocity = velocity
targetContentOffset.pointee = scrollView.contentOffset
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
}
func scrollViewShouldScroll(toTop scrollView: UIScrollView) -> Bool {
return false
}
func update(layout: ContainerViewLayout, controllers: [ViewController], coveredByModalTransition: CGFloat, transition: ContainedViewLayoutTransition) {
if self.isDismissed {
return
}
self.isUpdatingState = true
self.validLayout = layout
var isStandaloneModal = false
var flatReceivesModalTransition = false
if let controller = controllers.first {
if case .standaloneModal = controller.navigationPresentation {
isStandaloneModal = true
}
if controller.flatReceivesModalTransition {
flatReceivesModalTransition = true
}
}
let _ = flatReceivesModalTransition
transition.updateFrame(node: self.dim, frame: CGRect(origin: CGPoint(), size: layout.size))
self.ignoreScrolling = true
self.scrollNode.view.isScrollEnabled = (layout.inputHeight == nil || layout.inputHeight == 0.0) && self.isInteractiveDimissEnabled && !self.isFlat
let previousBounds = self.scrollNode.bounds
let scrollNodeFrame = CGRect(origin: CGPoint(x: self.horizontalDismissOffset ?? 0.0, y: 0.0), size: layout.size)
self.scrollNode.frame = scrollNodeFrame
self.scrollNode.view.contentSize = CGSize(width: layout.size.width, height: layout.size.height * 2.0)
if !self.scrollNode.view.isDecelerating && !self.scrollNode.view.isDragging {
let defaultBounds = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height), size: layout.size)
if self.scrollNode.bounds != defaultBounds {
self.scrollNode.bounds = defaultBounds
}
if previousBounds.minY != defaultBounds.minY {
transition.animateOffsetAdditive(node: self.scrollNode, offset: previousBounds.minY - defaultBounds.minY)
}
}
self.ignoreScrolling = false
self.scrollNode.view.isScrollEnabled = !isStandaloneModal && !self.isFlat
let isLandscape = layout.orientation == .landscape
let containerLayout: ContainerViewLayout
let containerFrame: CGRect
let containerScale: CGFloat
if layout.metrics.widthClass == .compact || self.isFlat {
self.panRecognizer?.isEnabled = true
self.container.clipsToBounds = true
if self.isFlat {
self.dim.backgroundColor = .clear
} else {
self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25)
}
if isStandaloneModal || isLandscape || (self.isFlat && !flatReceivesModalTransition) {
self.container.cornerRadius = 0.0
} else {
var cornerRadius: CGFloat = 10.0
if let controller = controllers.first, controller._hasGlassStyle {
cornerRadius = 38.0
}
self.container.cornerRadius = cornerRadius
}
if #available(iOS 11.0, *) {
if layout.safeInsets.bottom.isZero {
self.container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
} else {
self.container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
}
}
var topInset: CGFloat
if isStandaloneModal || isLandscape {
topInset = 0.0
containerLayout = layout
let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: containerLayout.size)
containerScale = 1.0
containerFrame = unscaledFrame
} else {
topInset = 10.0
if self.isFlat {
topInset = 0.0
} else if let statusBarHeight = layout.statusBarHeight {
topInset += statusBarHeight
}
let effectiveStatusBarHeight: CGFloat?
if self.isFlat {
effectiveStatusBarHeight = layout.statusBarHeight
} else {
effectiveStatusBarHeight = nil
}
containerLayout = ContainerViewLayout(size: CGSize(width: layout.size.width, height: layout.size.height - topInset), metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: layout.intrinsicInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.intrinsicInsets.right), safeInsets: UIEdgeInsets(top: 0.0, left: layout.safeInsets.left, bottom: layout.safeInsets.bottom, right: layout.safeInsets.right), additionalInsets: layout.additionalInsets, statusBarHeight: effectiveStatusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver)
let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset - coveredByModalTransition * 10.0), size: containerLayout.size)
let maxScale: CGFloat = (containerLayout.size.width - 16.0 * 2.0) / containerLayout.size.width
containerScale = 1.0 * (1.0 - coveredByModalTransition) + maxScale * coveredByModalTransition
var maxScaledTopInset: CGFloat = topInset - 10.0
if flatReceivesModalTransition {
maxScaledTopInset = 0.0
if let statusBarHeight = layout.statusBarHeight {
maxScaledTopInset += statusBarHeight
}
}
let scaledTopInset: CGFloat = topInset * (1.0 - coveredByModalTransition) + maxScaledTopInset * coveredByModalTransition
containerFrame = unscaledFrame.offsetBy(dx: 0.0, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0))
}
} else {
self.panRecognizer?.isEnabled = false
if self.isFlat && !flatReceivesModalTransition {
self.dim.backgroundColor = .clear
self.container.clipsToBounds = true
self.container.cornerRadius = 0.0
if #available(iOS 11.0, *) {
self.container.layer.maskedCorners = []
}
} else {
self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.4)
self.container.clipsToBounds = true
self.container.cornerRadius = 10.0
if #available(iOS 11.0, *) {
self.container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
}
}
let verticalInset: CGFloat = 44.0
let maxSide = max(layout.size.width, layout.size.height)
let minSide = min(layout.size.width, layout.size.height)
var containerSize = CGSize(width: min(layout.size.width - 20.0, floor(maxSide / 2.0)), height: min(layout.size.height, minSide) - verticalInset * 2.0)
if let preferredSize = controllers.last?.preferredContentSizeForLayout(layout) {
containerSize = preferredSize
}
containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize)
containerScale = 1.0
var inputHeight: CGFloat?
if let inputHeightValue = layout.inputHeight {
inputHeight = max(0.0, inputHeightValue - (layout.size.height - containerFrame.maxY))
}
let effectiveStatusBarHeight: CGFloat?
if self.isFlat {
effectiveStatusBarHeight = layout.statusBarHeight
} else {
effectiveStatusBarHeight = nil
}
containerLayout = ContainerViewLayout(size: containerSize, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: effectiveStatusBarHeight, inputHeight: inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver)
}
transition.updateFrameAsPositionAndBounds(node: self.container, frame: containerFrame.offsetBy(dx: 0.0, dy: layout.size.height))
transition.updateTransformScale(node: self.container, scale: containerScale)
self.container.update(layout: containerLayout, canBeClosed: true, controllers: controllers, transition: transition)
self.isUpdatingState = false
}
func animateIn(transition: ContainedViewLayoutTransition) {
if let controller = self.container.controllers.first, case .standaloneModal = controller.navigationPresentation {
} else if self.isFlat {
} else {
transition.updateAlpha(node: self.dim, alpha: 1.0)
transition.animatePositionAdditive(node: self.container, offset: CGPoint(x: 0.0, y: self.bounds.height + self.container.bounds.height / 2.0 - (self.container.position.y - self.bounds.height)))
}
}
func dismiss(transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) -> ContainedViewLayoutTransition {
for controller in self.container.controllers {
controller.viewWillDisappear(transition.isAnimated)
}
if let firstController = self.container.controllers.first, case .standaloneModal = firstController.navigationPresentation {
for controller in self.container.controllers {
controller.setIgnoreAppearanceMethodInvocations(true)
controller.displayNode.removeFromSupernode()
controller.setIgnoreAppearanceMethodInvocations(false)
controller.viewDidDisappear(transition.isAnimated)
}
completion()
return transition
} else {
if transition.isAnimated && !self.isFlat {
let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
alphaTransition.updateAlpha(node: self.dim, alpha: 0.0, beginWithCurrentState: true)
if let lastController = self.container.controllers.last as? MinimizableController, lastController.isMinimized {
self.dim.layer.removeAllAnimations()
}
positionTransition.updatePosition(node: self.container, position: CGPoint(x: self.container.position.x, y: self.bounds.height + self.container.bounds.height / 2.0 + self.bounds.height), beginWithCurrentState: true, completion: { [weak self] _ in
guard let strongSelf = self else {
return
}
for controller in strongSelf.container.controllers {
controller.viewDidDisappear(transition.isAnimated)
}
completion()
})
return positionTransition
} else {
for controller in self.container.controllers {
controller.setIgnoreAppearanceMethodInvocations(true)
controller.displayNode.removeFromSupernode()
controller.setIgnoreAppearanceMethodInvocations(false)
controller.viewDidDisappear(transition.isAnimated)
}
completion()
return transition
}
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let result = super.hitTest(point, with: event) else {
return nil
}
if !self.container.bounds.contains(self.view.convert(point, to: self.container.view)) {
return self.dim.view
}
if self.isFlat {
if result === self.container.view {
return nil
}
return result
}
var currentParent: UIView? = result
var enableScrolling = true
while true {
if currentParent == nil {
break
}
if currentParent is UIKeyInput {
if currentParent?.disablesInteractiveModalDismiss == true {
enableScrolling = false
break
}
} else if let scrollView = currentParent as? UIScrollView {
if scrollView === self.scrollNode.view {
break
}
if scrollView.disablesInteractiveModalDismiss {
enableScrolling = false
break
} else {
if scrollView.isDecelerating && scrollView.contentOffset.y < -scrollView.contentInset.top {
return self.scrollNode.view
}
}
} else if let listView = currentParent as? ListViewBackingView, let listNode = listView.target {
if listNode.view.disablesInteractiveModalDismiss {
enableScrolling = false
break
} else if listNode.scroller.isDecelerating && listNode.scroller.contentOffset.y < listNode.scroller.contentInset.top {
return self.scrollNode.view
}
} else if let currentParent, currentParent.disablesInteractiveModalDismiss {
enableScrolling = false
break
}
currentParent = currentParent?.superview
}
if let controller = self.container.controllers.last {
if controller.view.disablesInteractiveModalDismiss {
enableScrolling = false
}
}
self.isInteractiveDimissEnabled = enableScrolling
if let layout = self.validLayout {
if layout.inputHeight != nil && layout.inputHeight != 0.0 {
enableScrolling = false
}
}
self.scrollNode.view.isScrollEnabled = enableScrolling && !self.isFlat
return result
}
}
@@ -0,0 +1,148 @@
import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
private enum CornerType {
case topLeft
case topRight
case bottomLeft
case bottomRight
}
private func generateCornerImage(radius: CGFloat, type: CornerType) -> UIImage? {
return generateImage(CGSize(width: radius, height: radius), rotatedContext: { size, context in
context.setFillColor(UIColor.black.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
UIGraphicsPushContext(context)
let origin: CGPoint
switch type {
case .topLeft:
origin = CGPoint()
case .topRight:
origin = CGPoint(x: -radius, y: 0.0)
case .bottomLeft:
origin = CGPoint(x: 0.0, y: -radius)
case .bottomRight:
origin = CGPoint(x: -radius, y: -radius)
}
UIBezierPath(roundedRect: CGRect(origin: origin, size: CGSize(width: radius * 2.0, height: radius * 2.0)), cornerRadius: radius).fill()
UIGraphicsPopContext()
})
}
public final class NavigationModalFrame: ASDisplayNode {
private let topShade: ASDisplayNode
private let leftShade: ASDisplayNode
private let rightShade: ASDisplayNode
private let bottomShade: ASDisplayNode
private let topLeftCorner: ASImageNode
private let topRightCorner: ASImageNode
private let bottomLeftCorner: ASImageNode
private let bottomRightCorner: ASImageNode
private var currentMaxCornerRadius: CGFloat?
private var progress: CGFloat = 1.0
private var additionalProgress: CGFloat = 0.0
private var validLayout: ContainerViewLayout?
override public init() {
self.topShade = ASDisplayNode()
self.topShade.backgroundColor = .black
self.leftShade = ASDisplayNode()
self.leftShade.backgroundColor = .black
self.rightShade = ASDisplayNode()
self.rightShade.backgroundColor = .black
self.bottomShade = ASDisplayNode()
self.bottomShade.backgroundColor = .black
self.topLeftCorner = ASImageNode()
self.topLeftCorner.displaysAsynchronously = false
self.topRightCorner = ASImageNode()
self.topRightCorner.displaysAsynchronously = false
self.bottomLeftCorner = ASImageNode()
self.bottomLeftCorner.displaysAsynchronously = false
self.bottomRightCorner = ASImageNode()
self.bottomRightCorner.displaysAsynchronously = false
super.init()
self.addSubnode(self.topShade)
self.addSubnode(self.leftShade)
self.addSubnode(self.rightShade)
self.addSubnode(self.bottomShade)
self.addSubnode(self.topLeftCorner)
self.addSubnode(self.topRightCorner)
self.addSubnode(self.bottomLeftCorner)
self.addSubnode(self.bottomRightCorner)
}
public func update(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
self.validLayout = layout
self.updateShades(layout: layout, progress: 1.0 - self.progress, additionalProgress: self.additionalProgress, transition: transition, completion: {})
}
public func updateDismissal(transition: ContainedViewLayoutTransition, progress: CGFloat, additionalProgress: CGFloat, completion: @escaping () -> Void) {
self.progress = progress
self.additionalProgress = additionalProgress
if let layout = self.validLayout {
self.updateShades(layout: layout, progress: 1.0 - progress, additionalProgress: additionalProgress, transition: transition, completion: completion)
} else {
completion()
}
}
private func updateShades(layout: ContainerViewLayout, progress: CGFloat, additionalProgress: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
let sideInset: CGFloat = 16.0
var topInset: CGFloat = 0.0
if let statusBarHeight = layout.statusBarHeight {
topInset += statusBarHeight
}
let additionalTopInset: CGFloat = 10.0
let contentScale = (layout.size.width - sideInset * 2.0) / layout.size.width
let bottomInset: CGFloat = layout.size.height - contentScale * layout.size.height - topInset
let cornerRadius: CGFloat = 28.0
let initialCornerRadius: CGFloat
if !layout.safeInsets.top.isZero {
initialCornerRadius = layout.deviceMetrics.screenCornerRadius
} else {
initialCornerRadius = 0.0
}
if self.currentMaxCornerRadius != cornerRadius {
self.topLeftCorner.image = generateCornerImage(radius: max(initialCornerRadius, cornerRadius), type: .topLeft)
self.topRightCorner.image = generateCornerImage(radius: max(initialCornerRadius, cornerRadius), type: .topRight)
self.bottomLeftCorner.image = generateCornerImage(radius: max(initialCornerRadius, cornerRadius), type: .bottomLeft)
self.bottomRightCorner.image = generateCornerImage(radius: max(initialCornerRadius, cornerRadius), type: .bottomRight)
}
let cornerSize = progress * cornerRadius + (1.0 - progress) * initialCornerRadius
let cornerSideOffset: CGFloat = progress * sideInset + additionalProgress * sideInset
let cornerTopOffset: CGFloat = progress * topInset + additionalProgress * additionalTopInset
let cornerBottomOffset: CGFloat = progress * bottomInset
transition.updateFrame(node: self.topLeftCorner, frame: CGRect(origin: CGPoint(x: cornerSideOffset, y: cornerTopOffset), size: CGSize(width: cornerSize, height: cornerSize)), beginWithCurrentState: true)
transition.updateFrame(node: self.topRightCorner, frame: CGRect(origin: CGPoint(x: layout.size.width - cornerSideOffset - cornerSize, y: cornerTopOffset), size: CGSize(width: cornerSize, height: cornerSize)), beginWithCurrentState: true)
transition.updateFrame(node: self.bottomLeftCorner, frame: CGRect(origin: CGPoint(x: cornerSideOffset, y: layout.size.height - cornerBottomOffset - cornerSize), size: CGSize(width: cornerSize, height: cornerSize)), beginWithCurrentState: true)
transition.updateFrame(node: self.bottomRightCorner, frame: CGRect(origin: CGPoint(x: layout.size.width - cornerSideOffset - cornerSize, y: layout.size.height - cornerBottomOffset - cornerSize), size: CGSize(width: cornerSize, height: cornerSize)), beginWithCurrentState: true)
let topShadeOffset: CGFloat = progress * topInset + additionalProgress * additionalTopInset
let bottomShadeOffset: CGFloat = progress * bottomInset
let leftShadeOffset: CGFloat = progress * sideInset + additionalProgress * sideInset
let rightShadeWidth: CGFloat = progress * sideInset + additionalProgress * sideInset
let rightShadeOffset: CGFloat = layout.size.width - rightShadeWidth
transition.updateFrame(node: self.topShade, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: topShadeOffset)), beginWithCurrentState: true)
transition.updateFrame(node: self.bottomShade, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomShadeOffset), size: CGSize(width: layout.size.width, height: bottomShadeOffset)))
transition.updateFrame(node: self.leftShade, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: leftShadeOffset, height: layout.size.height)), beginWithCurrentState: true)
transition.updateFrame(node: self.rightShade, frame: CGRect(origin: CGPoint(x: rightShadeOffset, y: 0.0), size: CGSize(width: rightShadeWidth, height: layout.size.height)), beginWithCurrentState: true, completion: { _ in
completion()
})
}
}
@@ -0,0 +1,100 @@
import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
final class NavigationOverlayContainer: ASDisplayNode {
let controller: ViewController
let blocksInteractionUntilReady: Bool
private(set) var isReady: Bool = false
var isRemoved: Bool = false
var isReadyUpdated: (() -> Void)?
private var isReadyDisposable: Disposable?
private var validLayout: ContainerViewLayout?
private var isUpdatingState: Bool = false
var keyboardViewManager: KeyboardViewManager? {
didSet {
if self.keyboardViewManager !== oldValue {
}
}
}
init(controller: ViewController, blocksInteractionUntilReady: Bool, controllerRemoved: @escaping (ViewController) -> Void, statusBarUpdated: @escaping (ContainedViewLayoutTransition) -> Void, modalStyleOverlayTransitionFactorUpdated: @escaping (ContainedViewLayoutTransition) -> Void) {
self.controller = controller
self.blocksInteractionUntilReady = blocksInteractionUntilReady
super.init()
self.controller.navigation_setDismiss({ [weak self] in
guard let strongSelf = self else {
return
}
controllerRemoved(strongSelf.controller)
}, rootController: nil)
self.controller.statusBar.alphaUpdated = { transition in
statusBarUpdated(transition)
}
self.controller.modalStyleOverlayTransitionFactorUpdated = { transition in
modalStyleOverlayTransitionFactorUpdated(transition)
}
self.isReadyDisposable = (self.controller.ready.get()
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] _ in
guard let strongSelf = self else {
return
}
if !strongSelf.isReady {
strongSelf.isReady = true
if !strongSelf.isUpdatingState {
strongSelf.isReadyUpdated?()
}
}
}).strict()
}
deinit {
self.isReadyDisposable?.dispose()
}
override func didLoad() {
super.didLoad()
}
func update(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
self.isUpdatingState = true
let updateLayout = self.validLayout != layout
self.validLayout = layout
if updateLayout {
transition.updateFrame(node: self.controller.displayNode, frame: CGRect(origin: CGPoint(), size: layout.size))
self.controller.containerLayoutUpdated(layout, transition: transition)
}
self.isUpdatingState = false
}
func transitionIn() {
self.controller.viewWillAppear(false)
self.controller.setIgnoreAppearanceMethodInvocations(true)
self.addSubnode(self.controller.displayNode)
self.controller.setIgnoreAppearanceMethodInvocations(false)
self.controller.viewDidAppear(false)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let result = self.controller.view.hitTest(point, with: event) {
return result
}
return nil
}
}
@@ -0,0 +1,130 @@
import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
enum NavigationSplitContainerScrollToTop {
case master
case detail
}
final class NavigationSplitContainer: ASDisplayNode {
private var theme: NavigationControllerTheme
private let masterScrollToTopView: ScrollToTopView
private let detailScrollToTopView: ScrollToTopView
private let masterContainer: NavigationContainer
private let detailContainer: NavigationContainer
private let separator: ASDisplayNode
private(set) var masterControllers: [ViewController] = []
private(set) var detailControllers: [ViewController] = []
var canHaveKeyboardFocus: Bool = false {
didSet {
self.masterContainer.canHaveKeyboardFocus = self.canHaveKeyboardFocus
self.detailContainer.canHaveKeyboardFocus = self.canHaveKeyboardFocus
}
}
var isInFocus: Bool = false {
didSet {
if self.isInFocus != oldValue {
self.inFocusUpdated(isInFocus: self.isInFocus)
}
}
}
func inFocusUpdated(isInFocus: Bool) {
self.masterContainer.isInFocus = isInFocus
self.detailContainer.isInFocus = isInFocus
}
init(theme: NavigationControllerTheme, controllerRemoved: @escaping (ViewController) -> Void, scrollToTop: @escaping (NavigationSplitContainerScrollToTop) -> Void) {
self.theme = theme
self.masterScrollToTopView = ScrollToTopView(frame: CGRect())
self.masterScrollToTopView.action = {
scrollToTop(.master)
}
self.detailScrollToTopView = ScrollToTopView(frame: CGRect())
self.detailScrollToTopView.action = {
scrollToTop(.detail)
}
self.masterContainer = NavigationContainer(isFlat: false, controllerRemoved: controllerRemoved)
self.masterContainer.clipsToBounds = true
self.detailContainer = NavigationContainer(isFlat: false, controllerRemoved: controllerRemoved)
self.detailContainer.clipsToBounds = true
self.separator = ASDisplayNode()
self.separator.backgroundColor = theme.navigationBar.separatorColor
super.init()
self.addSubnode(self.masterContainer)
self.addSubnode(self.detailContainer)
self.addSubnode(self.separator)
self.view.addSubview(self.masterScrollToTopView)
self.view.addSubview(self.detailScrollToTopView)
}
func hasNonReadyControllers() -> Bool {
if self.masterContainer.hasNonReadyControllers() {
return true
}
if self.detailContainer.hasNonReadyControllers() {
return true
}
return false
}
func updateTheme(theme: NavigationControllerTheme) {
self.separator.backgroundColor = theme.navigationBar.separatorColor
}
func update(layout: ContainerViewLayout, masterControllers: [ViewController], detailControllers: [ViewController], detailsPlaceholderNode: NavigationDetailsPlaceholderNode?, transition: ContainedViewLayoutTransition) {
let masterWidth: CGFloat = min(max(320.0, floor(layout.size.width / 3.0)), floor(layout.size.width / 2.0))
let detailWidth = layout.size.width - masterWidth
self.masterScrollToTopView.frame = CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: masterWidth, height: 1.0))
self.detailScrollToTopView.frame = CGRect(origin: CGPoint(x: masterWidth, y: -1.0), size: CGSize(width: detailWidth, height: 1.0))
transition.updateFrame(node: self.masterContainer, frame: CGRect(origin: CGPoint(), size: CGSize(width: masterWidth, height: layout.size.height)))
transition.updateFrame(node: self.detailContainer, frame: CGRect(origin: CGPoint(x: masterWidth, y: 0.0), size: CGSize(width: detailWidth, height: layout.size.height)))
transition.updateFrame(node: self.separator, frame: CGRect(origin: CGPoint(x: masterWidth, y: 0.0), size: CGSize(width: UIScreenPixel, height: layout.size.height)))
if let detailsPlaceholderNode {
let needsTiling = layout.size.width > layout.size.height
detailsPlaceholderNode.updateLayout(size: CGSize(width: detailWidth, height: layout.size.height), needsTiling: needsTiling, transition: transition)
transition.updateFrame(node: detailsPlaceholderNode, frame: CGRect(origin: CGPoint(x: masterWidth, y: 0.0), size: CGSize(width: detailWidth, height: layout.size.height)))
}
self.masterContainer.update(layout: ContainerViewLayout(size: CGSize(width: masterWidth, height: layout.size.height), metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, additionalInsets: UIEdgeInsets(), statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), canBeClosed: false, controllers: masterControllers, transition: transition)
self.detailContainer.update(layout: ContainerViewLayout(size: CGSize(width: detailWidth, height: layout.size.height), metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), canBeClosed: true, controllers: detailControllers, transition: transition)
var controllersUpdated = false
if self.detailControllers.last !== detailControllers.last {
controllersUpdated = true
} else if self.masterControllers.count != masterControllers.count {
controllersUpdated = true
} else {
for i in 0 ..< masterControllers.count {
if masterControllers[i] !== self.masterControllers[i] {
controllersUpdated = true
break
}
}
}
self.masterControllers = masterControllers
self.detailControllers = detailControllers
if controllersUpdated {
let data = self.detailControllers.last?.customData
for controller in self.masterControllers {
controller.updateNavigationCustomData(data, progress: 1.0, transition: transition)
}
}
}
}
@@ -0,0 +1,150 @@
import UIKit
import AsyncDisplayKit
import AppBundle
public class NavigationBackButtonNode: ASControlNode {
private func fontForCurrentState() -> UIFont {
return UIFont.systemFont(ofSize: 17.0)
}
private func attributesForCurrentState() -> [NSAttributedString.Key : AnyObject] {
return [
NSAttributedString.Key.font: self.fontForCurrentState(),
NSAttributedString.Key.foregroundColor: self.isEnabled ? self.color : self.disabledColor
]
}
let arrow: ASDisplayNode
public let label: ImmediateTextNode
private let arrowSpacing: CGFloat = 4.0
private var _text: String = ""
public var text: String {
get {
return self._text
}
set(value) {
self._text = value
self.label.attributedText = NSAttributedString(string: text, attributes: self.attributesForCurrentState())
self.invalidateCalculatedLayout()
}
}
public var color: UIColor = UIColor(rgb: 0x0088ff) {
didSet {
self.label.attributedText = NSAttributedString(string: self._text, attributes: self.attributesForCurrentState())
}
}
public var disabledColor: UIColor = UIColor(rgb: 0xd0d0d0) {
didSet {
self.label.attributedText = NSAttributedString(string: self._text, attributes: self.attributesForCurrentState())
}
}
private var touchCount = 0
var pressed: () -> () = {}
override public init() {
self.arrow = ASDisplayNode()
self.label = ImmediateTextNode()
super.init()
self.isUserInteractionEnabled = true
self.isExclusiveTouch = true
self.hitTestSlop = UIEdgeInsets(top: -16.0, left: -10.0, bottom: -16.0, right: -10.0)
self.displaysAsynchronously = false
self.arrow.displaysAsynchronously = false
self.label.displaysAsynchronously = false
self.addSubnode(self.arrow)
let arrowImage = UIImage(named: "NavigationBackArrowLight", in: getAppBundle(), compatibleWith: nil)?.precomposed()
self.arrow.contents = arrowImage?.cgImage
self.arrow.frame = CGRect(origin: CGPoint(), size: arrowImage?.size ?? CGSize())
self.addSubnode(self.label)
}
public override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
let _ = self.label.updateLayout(CGSize(width: max(0.0, constrainedSize.width - self.arrow.frame.size.width - self.arrowSpacing), height: constrainedSize.height))
return CGSize(width: self.arrow.frame.size.width + self.arrowSpacing + self.label.calculatedSize.width, height: max(self.arrow.frame.size.height, self.label.calculatedSize.height))
}
var labelFrame: CGRect {
get {
return CGRect(x: self.arrow.frame.size.width + self.arrowSpacing, y: floor((self.frame.size.height - self.label.calculatedSize.height) / 2.0), width: self.label.calculatedSize.width, height: self.label.calculatedSize.height)
}
}
public override func layout() {
super.layout()
self.arrow.frame = CGRect(x: 0.0, y: floor((self.frame.size.height - arrow.frame.size.height) / 2.0), width: self.arrow.frame.size.width, height: self.arrow.frame.size.height)
self.label.frame = self.labelFrame
}
private func touchInsideApparentBounds(_ touch: UITouch) -> Bool {
var apparentBounds = self.bounds
let hitTestSlop = self.hitTestSlop
apparentBounds.origin.x += hitTestSlop.left
apparentBounds.size.width -= hitTestSlop.left + hitTestSlop.right
apparentBounds.origin.y += hitTestSlop.top
apparentBounds.size.height -= hitTestSlop.top + hitTestSlop.bottom
return apparentBounds.contains(touch.location(in: self.view))
}
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
self.touchCount += touches.count
self.updateHighlightedState(true, animated: false)
}
public override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
self.updateHighlightedState(self.touchInsideApparentBounds(touches.first!), animated: true)
}
public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
self.updateHighlightedState(false, animated: false)
let previousTouchCount = self.touchCount
self.touchCount = max(0, self.touchCount - touches.count)
if previousTouchCount != 0 && self.touchCount == 0 && self.isEnabled && self.touchInsideApparentBounds(touches.first!) {
self.pressed()
}
}
public override func touchesCancelled(_ touches: Set<UITouch>?, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
self.touchCount = max(0, self.touchCount - (touches?.count ?? 0))
self.updateHighlightedState(false, animated: false)
}
private var _highlighted = false
private func updateHighlightedState(_ highlighted: Bool, animated: Bool) {
if _highlighted != highlighted {
_highlighted = highlighted
let alpha: CGFloat = !self.isEnabled ? 1.0 : (highlighted ? 0.4 : 1.0)
if animated {
UIView.animate(withDuration: 0.3, delay: 0.0, options: UIView.AnimationOptions.beginFromCurrentState, animations: { () -> Void in
self.alpha = alpha
}, completion: nil)
}
else {
self.alpha = alpha
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,65 @@
import Foundation
import UIKit
import AsyncDisplayKit
public final class NavigationBarBadgeNode: ASDisplayNode {
private var fillColor: UIColor
private var strokeColor: UIColor
private var textColor: UIColor
private let textNode: ImmediateTextNode
private let backgroundNode: ASImageNode
private let font: UIFont = Font.regular(13.0)
var text: String = "" {
didSet {
self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor)
self.invalidateCalculatedLayout()
}
}
public init(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) {
self.fillColor = fillColor
self.strokeColor = strokeColor
self.textColor = textColor
self.textNode = ImmediateTextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = false
self.backgroundNode = ASImageNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.image = generateStretchableFilledCircleImage(radius: 9.0, color: fillColor, backgroundColor: nil)
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.textNode)
}
func updateTheme(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) {
self.fillColor = fillColor
self.strokeColor = strokeColor
self.textColor = textColor
self.backgroundNode.image = generateStretchableFilledCircleImage(radius: 9.0, color: fillColor, backgroundColor: nil)
self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor)
self.textNode.redrawIfPossible()
}
override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
let badgeSize = self.textNode.updateLayout(constrainedSize)
let backgroundSize: CGSize
if self.text.count < 2 {
backgroundSize = CGSize(width: 18.0, height: 18.0)
} else {
backgroundSize = CGSize(width: max(18.0, badgeSize.width + 10.0 + 1.0), height: 18.0)
}
let backgroundFrame = CGRect(origin: CGPoint(), size: backgroundSize)
self.backgroundNode.frame = backgroundFrame
self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(backgroundFrame.midX - badgeSize.width / 2.0), y: floorToScreenPixels((backgroundFrame.size.height - badgeSize.height) / 2.0)), size: badgeSize)
return backgroundSize
}
}
@@ -0,0 +1,31 @@
import Foundation
import UIKit
import AsyncDisplayKit
public enum NavigationBarContentMode {
case replacement
case expansion
}
open class NavigationBarContentNode: ASDisplayNode {
open var requestContainerLayout: (ContainedViewLayoutTransition) -> Void = { _ in }
open var height: CGFloat {
return self.nominalHeight
}
open var clippedHeight: CGFloat {
return self.nominalHeight
}
open var nominalHeight: CGFloat {
return 44.0
}
open var mode: NavigationBarContentMode {
return .replacement
}
open func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
}
}
@@ -0,0 +1,6 @@
import Foundation
import AsyncDisplayKit
public protocol NavigationBarTitleTransitionNode {
func makeTransitionMirrorNode() -> ASDisplayNode
}
@@ -0,0 +1,8 @@
import Foundation
import UIKit
public protocol NavigationBarTitleView {
func animateLayoutTransition()
func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) -> CGRect
}
@@ -0,0 +1,69 @@
import Foundation
import UIKit
import AsyncDisplayKit
class NavigationBarTransitionContainer: ASDisplayNode {
var progress: CGFloat = 0.0 {
didSet {
self.layout()
}
}
let transition: NavigationTransition
let topNavigationBar: NavigationBar
let bottomNavigationBar: NavigationBar
let topClippingNode: ASDisplayNode
let bottomClippingNode: ASDisplayNode
let topNavigationBarSupernode: ASDisplayNode?
let bottomNavigationBarSupernode: ASDisplayNode?
init(transition: NavigationTransition, topNavigationBar: NavigationBar, bottomNavigationBar: NavigationBar) {
self.transition = transition
self.topNavigationBar = topNavigationBar
self.topNavigationBarSupernode = topNavigationBar.supernode
self.bottomNavigationBar = bottomNavigationBar
self.bottomNavigationBarSupernode = bottomNavigationBar.supernode
self.topClippingNode = ASDisplayNode()
self.topClippingNode.clipsToBounds = true
self.bottomClippingNode = ASDisplayNode()
self.bottomClippingNode.clipsToBounds = true
super.init()
self.topClippingNode.addSubnode(self.topNavigationBar)
self.bottomClippingNode.addSubnode(self.bottomNavigationBar)
self.addSubnode(self.bottomClippingNode)
self.addSubnode(self.topClippingNode)
}
func complete() {
self.topNavigationBarSupernode?.addSubnode(self.topNavigationBar)
self.bottomNavigationBarSupernode?.addSubnode(self.bottomNavigationBar)
}
override func layout() {
super.layout()
let size = self.bounds.size
let position: CGFloat
switch self.transition {
case .Push:
position = 1.0 - progress
case .Pop:
position = progress
}
let offset = floorToScreenPixels(size.width * position)
self.topClippingNode.frame = CGRect(origin: CGPoint(x: offset, y: 0.0), size: size)
self.bottomClippingNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: offset, height: size.height))
}
}
@@ -0,0 +1,21 @@
import Foundation
import UIKit
enum NavigationBarTransitionRole {
case top
case bottom
}
final class NavigationBarTransitionState {
weak var navigationBar: NavigationBar?
let transition: NavigationTransition
let role: NavigationBarTransitionRole
let progress: CGFloat
init(navigationBar: NavigationBar, transition: NavigationTransition, role: NavigationBarTransitionRole, progress: CGFloat) {
self.navigationBar = navigationBar
self.transition = transition
self.role = role
self.progress = progress
}
}
@@ -0,0 +1,567 @@
import UIKit
import AsyncDisplayKit
public protocol NavigationButtonCustomDisplayNode {
var isHighlightable: Bool { get }
}
private final class NavigationButtonItemNode: ImmediateTextNode {
private func fontForCurrentState() -> UIFont {
return self.bold ? UIFont.boldSystemFont(ofSize: 17.0) : UIFont.systemFont(ofSize: 17.0)
}
private func attributesForCurrentState() -> [NSAttributedString.Key: AnyObject] {
return [
NSAttributedString.Key.font: self.fontForCurrentState(),
NSAttributedString.Key.foregroundColor: self.isEnabled ? self.color : self.disabledColor
]
}
private var setEnabledListener: Int?
var item: UIBarButtonItem? {
didSet {
if self.item !== oldValue {
if let oldValue = oldValue, let setEnabledListener = self.setEnabledListener {
oldValue.removeSetEnabledListener(setEnabledListener)
self.setEnabledListener = nil
}
if let item = self.item {
self.setEnabledListener = item.addSetEnabledListener { [weak self] value in
self?.isEnabled = value
}
self.accessibilityHint = item.accessibilityHint
self.accessibilityLabel = item.accessibilityLabel
}
}
}
}
private var _text: String?
public var text: String {
get {
return _text ?? ""
}
set(value) {
_text = value
self.attributedText = NSAttributedString(string: text, attributes: self.attributesForCurrentState())
if _image == nil {
if self.item?.accessibilityLabel == nil {
self.item?.accessibilityLabel = value
}
}
}
}
private(set) var imageNode: ASImageNode?
private let imageRippleNode: ASImageNode
private var _image: UIImage?
public var image: UIImage? {
get {
return _image
} set(value) {
_image = value
if let _ = value {
if self.imageNode == nil {
let imageNode = ASImageNode()
imageNode.displayWithoutProcessing = true
imageNode.displaysAsynchronously = false
self.imageNode = imageNode
if self.imageRippleNode.supernode != nil {
self.imageRippleNode.image = nil
self.imageRippleNode.removeFromSupernode()
}
self.addSubnode(imageNode)
}
self.imageNode?.image = image
} else if let imageNode = self.imageNode {
imageNode.removeFromSupernode()
self.imageNode = nil
if self.imageRippleNode.supernode != nil {
self.imageRippleNode.image = nil
self.imageRippleNode.removeFromSupernode()
}
}
self.invalidateCalculatedLayout()
self.setNeedsLayout()
}
}
public var node: ASDisplayNode? {
didSet {
if self.node !== oldValue {
oldValue?.removeFromSupernode()
if let node = self.node {
self.addSubnode(node)
self.invalidateCalculatedLayout()
self.setNeedsLayout()
self.updatePointerInteraction()
}
}
}
}
public var color: UIColor = UIColor(rgb: 0x0088ff) {
didSet {
if let text = self._text {
self.attributedText = NSAttributedString(string: text, attributes: self.attributesForCurrentState())
}
}
}
public var rippleColor: UIColor = UIColor(rgb: 0x000000, alpha: 0.05) {
didSet {
if self.imageRippleNode.image != nil {
self.imageRippleNode.image = generateFilledCircleImage(diameter: 30.0, color: self.rippleColor)
}
}
}
public var disabledColor: UIColor = UIColor(rgb: 0xd0d0d0) {
didSet {
if let text = self._text {
self.attributedText = NSAttributedString(string: text, attributes: self.attributesForCurrentState())
}
}
}
private var _bold: Bool = false
public var bold: Bool {
get {
return _bold
}
set(value) {
if _bold != value {
_bold = value
self.attributedText = NSAttributedString(string: text, attributes: self.attributesForCurrentState())
}
}
}
private var touchCount = 0
public var pressed: () -> () = { }
public var highlightChanged: (Bool) -> () = { _ in }
override public var isAccessibilityElement: Bool {
get {
return true
} set(value) {
super.isAccessibilityElement = true
}
}
override public var accessibilityLabel: String? {
get {
if let item = self.item, let accessibilityLabel = item.accessibilityLabel {
return accessibilityLabel
} else {
return self.attributedText?.string
}
} set(value) {
}
}
override public var accessibilityHint: String? {
get {
if let item = self.item, let accessibilityHint = item.accessibilityHint {
return accessibilityHint
} else {
return nil
}
} set(value) {
}
}
var pointerInteraction: PointerInteraction?
override public init() {
self.imageRippleNode = ASImageNode()
self.imageRippleNode.displaysAsynchronously = false
self.imageRippleNode.displayWithoutProcessing = true
self.imageRippleNode.alpha = 0.0
super.init()
self.isAccessibilityElement = true
self.isUserInteractionEnabled = true
self.isExclusiveTouch = true
self.hitTestSlop = UIEdgeInsets(top: -16.0, left: -10.0, bottom: -16.0, right: -10.0)
self.displaysAsynchronously = false
self.verticalAlignment = .middle
self.accessibilityTraits = .button
}
override func didLoad() {
super.didLoad()
self.updatePointerInteraction()
}
func updatePointerInteraction() {
let pointerStyle: PointerStyle
if self.node != nil {
pointerStyle = .lift
} else {
pointerStyle = .insetRectangle(-8.0, 2.0)
}
self.pointerInteraction = PointerInteraction(node: self, style: pointerStyle)
}
override func updateLayout(_ constrainedSize: CGSize) -> CGSize {
var superSize = super.updateLayout(constrainedSize)
if let node = self.node {
let nodeSize = node.measure(constrainedSize)
let size = CGSize(width: max(nodeSize.width, superSize.width), height: max(nodeSize.height, superSize.height))
node.frame = CGRect(origin: CGPoint(), size: nodeSize)
return size
} else if let imageNode = self.imageNode {
let nodeSize = imageNode.image?.size ?? CGSize()
let size = CGSize(width: max(nodeSize.width, superSize.width), height: max(44.0, max(nodeSize.height, superSize.height)))
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - nodeSize.width) / 2.0), y: floorToScreenPixels((size.height - nodeSize.height) / 2.0)), size: nodeSize)
imageNode.frame = imageFrame
self.imageRippleNode.frame = imageFrame
return size
} else {
superSize.height = max(44.0, superSize.height)
}
return superSize
}
private func touchInsideApparentBounds(_ touch: UITouch) -> Bool {
var apparentBounds = self.bounds
let hitTestSlop = self.hitTestSlop
apparentBounds.origin.x += hitTestSlop.left
apparentBounds.size.width += -hitTestSlop.left - hitTestSlop.right
apparentBounds.origin.y += hitTestSlop.top
apparentBounds.size.height += -hitTestSlop.top - hitTestSlop.bottom
return apparentBounds.contains(touch.location(in: self.view))
}
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
self.touchCount += touches.count
self.updateHighlightedState(true, animated: false)
}
public override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
}
public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
self.updateHighlightedState(false, animated: false)
let previousTouchCount = self.touchCount
self.touchCount = max(0, self.touchCount - touches.count)
var touchInside = true
if let touch = touches.first {
touchInside = self.touchInsideApparentBounds(touch)
}
if previousTouchCount != 0 && self.touchCount == 0 && self.isEnabled && touchInside {
self.pressed()
}
}
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let node = self.node as? HighlightableButtonNode {
let result = node.view.hitTest(self.view.convert(point, to: node.view), with: event)
return result
} else {
let previousAlpha = self.alpha
self.alpha = 1.0
let result = super.hitTest(point, with: event)
self.alpha = previousAlpha
return result
}
}
public override func touchesCancelled(_ touches: Set<UITouch>?, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
self.touchCount = max(0, self.touchCount - (touches?.count ?? 0))
self.updateHighlightedState(false, animated: false)
}
private var _highlighted = false
private func updateHighlightedState(_ highlighted: Bool, animated: Bool) {
if _highlighted != highlighted {
_highlighted = highlighted
var shouldChangeHighlight = true
if let node = self.node as? NavigationButtonCustomDisplayNode {
shouldChangeHighlight = node.isHighlightable
}
if shouldChangeHighlight {
if self.alpha > 0.0 {
self.alpha = !self.isEnabled ? 1.0 : (highlighted ? 0.4 : 1.0)
}
self.highlightChanged(highlighted)
}
}
}
public var isEnabled: Bool = true {
didSet {
if self.isEnabled != oldValue {
self.attributedText = NSAttributedString(string: self.text, attributes: self.attributesForCurrentState())
if let constrainedSize = self.constrainedSize {
let _ = self.updateLayout(constrainedSize)
}
}
}
}
}
public final class NavigationButtonNode: ContextControllerSourceNode {
private var nodes: [NavigationButtonItemNode] = []
private var disappearingNodes: [(frame: CGRect, size: CGSize, node: NavigationButtonItemNode)] = []
public var singleCustomNode: ASDisplayNode? {
for node in self.nodes {
return node.node
}
return nil
}
public var mainContentNode: ASDisplayNode? {
return self.nodes.first
}
public var pressed: (Int) -> () = { _ in }
public var highlightChanged: (Int, Bool) -> () = { _, _ in }
public var color: UIColor = UIColor(rgb: 0x0088ff) {
didSet {
if !self.color.isEqual(oldValue) {
for node in self.nodes {
node.color = self.color
}
}
}
}
public var rippleColor: UIColor = UIColor(rgb: 0x000000, alpha: 0.05) {
didSet {
if !self.rippleColor.isEqual(oldValue) {
for node in self.nodes {
node.rippleColor = self.rippleColor
}
}
}
}
public var disabledColor: UIColor = UIColor(rgb: 0xd0d0d0) {
didSet {
if !self.disabledColor.isEqual(oldValue) {
for node in self.nodes {
node.disabledColor = self.disabledColor
}
}
}
}
override public var accessibilityElements: [Any]? {
get {
return self.nodes
} set(value) {
}
}
override public init() {
super.init()
self.isAccessibilityElement = false
self.isGestureEnabled = false
}
var manualText: String {
return self.nodes.first?.text ?? ""
}
var manualAlpha: CGFloat = 1.0 {
didSet {
for node in self.nodes {
node.alpha = self.manualAlpha
}
}
}
public var contentsColor: UIColor?
public func updateManualAlpha(alpha: CGFloat, transition: ContainedViewLayoutTransition) {
for node in self.nodes {
transition.updateAlpha(node: node, alpha: alpha)
}
}
func updateManualText(_ text: String, isBack: Bool = true) {
let node: NavigationButtonItemNode
if self.nodes.count > 0 {
node = self.nodes[0]
} else {
node = NavigationButtonItemNode()
node.color = self.color
node.rippleColor = self.rippleColor
node.layer.layerTintColor = self.contentsColor?.cgColor
node.highlightChanged = { [weak node, weak self] value in
if let strongSelf = self, let node = node {
if let index = strongSelf.nodes.firstIndex(where: { $0 === node }) {
strongSelf.highlightChanged(index, value)
}
}
}
node.pressed = { [weak self, weak node] in
if let strongSelf = self, let node = node {
if let index = strongSelf.nodes.firstIndex(where: { $0 === node }) {
strongSelf.pressed(index)
}
}
}
self.nodes.append(node)
self.addSubnode(node)
}
node.alpha = self.manualAlpha
node.item = nil
node.image = nil
node.text = text
node.bold = false
node.isEnabled = true
node.node = nil
node.hitTestSlop = isBack ? UIEdgeInsets(top: 0.0, left: -20.0, bottom: 0.0, right: 0.0) : UIEdgeInsets()
if 1 < self.nodes.count {
for i in 1 ..< self.nodes.count {
self.nodes[i].removeFromSupernode()
}
self.nodes.removeSubrange(1...)
}
}
func updateItems(_ items: [UIBarButtonItem], animated: Bool) {
for i in 0 ..< items.count {
let node: NavigationButtonItemNode
if self.nodes.count > i {
node = self.nodes[i]
} else {
node = NavigationButtonItemNode()
node.color = self.color
node.rippleColor = self.rippleColor
node.layer.layerTintColor = self.contentsColor?.cgColor
node.highlightChanged = { [weak node, weak self] value in
if let strongSelf = self, let node = node {
if let index = strongSelf.nodes.firstIndex(where: { $0 === node }) {
strongSelf.highlightChanged(index, value)
}
}
}
node.pressed = { [weak self, weak node] in
if let strongSelf = self, let node = node {
if let index = strongSelf.nodes.firstIndex(where: { $0 === node }) {
strongSelf.pressed(index)
}
}
}
self.nodes.append(node)
self.addSubnode(node)
}
node.alpha = self.manualAlpha
node.item = items[i]
node.image = items[i].image
node.text = items[i].title ?? ""
node.bold = items[i].style == .done
node.isEnabled = items[i].isEnabled
node.node = items[i].customDisplayNode
if animated {
node.layer.animateAlpha(from: 0.0, to: self.manualAlpha, duration: 0.16)
node.layer.animateScale(from: 0.001, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
}
}
if items.count < self.nodes.count {
for i in items.count ..< self.nodes.count {
let itemNode = self.nodes[i]
if animated {
disappearingNodes.append((itemNode.frame, self.bounds.size, itemNode))
itemNode.layer.animateAlpha(from: self.manualAlpha, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak self, weak itemNode] _ in
guard let itemNode else {
return
}
itemNode.removeFromSupernode()
guard let self else {
return
}
if let index = self.disappearingNodes.firstIndex(where: { $0.node === itemNode }) {
self.disappearingNodes.remove(at: index)
}
})
itemNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
} else {
itemNode.removeFromSupernode()
}
}
self.nodes.removeSubrange(items.count...)
}
}
public func updateLayout(constrainedSize: CGSize, isLandscape: Bool, isLeftAligned: Bool) -> CGSize {
var nodeOrigin = CGPoint()
var totalHeight: CGFloat = 0.0
for i in 0 ..< self.nodes.count {
if i != 0 {
nodeOrigin.x += 15.0
}
let node = self.nodes[i]
var nodeSize = node.updateLayout(constrainedSize)
nodeSize.width = ceil(nodeSize.width)
nodeSize.height = ceil(nodeSize.height)
totalHeight = max(totalHeight, nodeSize.height)
node.frame = CGRect(origin: CGPoint(x: nodeOrigin.x, y: floor((totalHeight - nodeSize.height) / 2.0)), size: nodeSize)
nodeOrigin.x += node.bounds.width
if isLandscape {
nodeOrigin.x += 16.0
}
if node.node == nil && node.imageNode != nil && i == self.nodes.count - 1 {
nodeOrigin.x -= 5.0
}
}
if !isLeftAligned {
for disappearingNode in self.disappearingNodes {
disappearingNode.node.frame = disappearingNode.frame.offsetBy(dx: nodeOrigin.x - disappearingNode.size.width, dy: (totalHeight - disappearingNode.size.height) * 0.5)
}
}
return CGSize(width: nodeOrigin.x, height: totalHeight)
}
func internalHitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.nodes.count == 1 {
return self.nodes[0].view
} else {
return super.hitTest(point, with: event)
}
}
}
@@ -0,0 +1,58 @@
import UIKit
import AsyncDisplayKit
public class NavigationTitleNode: ASDisplayNode {
private let label: ImmediateTextNode
private var _text: NSString = ""
public var text: NSString {
get {
return self._text
}
set(value) {
self._text = value
self.setText(value)
}
}
public var color: UIColor = UIColor.black {
didSet {
self.setText(self._text)
}
}
public init(text: NSString) {
self.label = ImmediateTextNode()
self.label.maximumNumberOfLines = 1
self.label.truncationType = .end
self.label.displaysAsynchronously = false
super.init()
self.addSubnode(self.label)
self.setText(text)
}
public required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setText(_ text: NSString) {
var titleAttributes = [NSAttributedString.Key : AnyObject]()
titleAttributes[NSAttributedString.Key.font] = UIFont.boldSystemFont(ofSize: 17.0)
titleAttributes[NSAttributedString.Key.foregroundColor] = self.color
let titleString = NSAttributedString(string: text as String, attributes: titleAttributes)
self.label.attributedText = titleString
self.invalidateCalculatedLayout()
}
public override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
let _ = self.label.updateLayout(constrainedSize)
return self.label.calculatedSize
}
public override func layout() {
self.label.frame = self.bounds
}
}
@@ -0,0 +1,349 @@
import UIKit
import AppBundle
import AsyncDisplayKit
enum NavigationTransition {
case Push
case Pop
}
private let shadowWidth: CGFloat = 16.0
private func generateShadow() -> UIImage? {
return generateImage(CGSize(width: 16.0, height: 1.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.black.cgColor)
context.setShadow(offset: CGSize(), blur: 16.0, color: UIColor(white: 0.0, alpha: 0.5).cgColor)
context.fill(CGRect(origin: CGPoint(x: size.width, y: 0.0), size: CGSize(width: 16.0, height: 1.0)))
})
}
private let shadowImage = generateShadow()
public protocol CustomNavigationTransitionNode: ASDisplayNode {
func setup(topNavigationBar: NavigationBar, bottomNavigationBar: NavigationBar)
func update(containerSize: CGSize, fraction: CGFloat, transition: ContainedViewLayoutTransition)
func restore()
}
final class NavigationTransitionCoordinator {
private var _progress: CGFloat = 0.0
var progress: CGFloat {
get {
return self._progress
}
}
private let container: NavigationContainer
private let transition: NavigationTransition
let isInteractive: Bool
let isFlat: Bool
let topNode: ASDisplayNode
let bottomNode: ASDisplayNode
private let topNavigationBar: NavigationBar?
private let bottomNavigationBar: NavigationBar?
private let dimNode: ASDisplayNode
private let shadowNode: ASImageNode
private let customTransitionNode: CustomNavigationTransitionNode?
private var topNodeInitialParameters: (clipsToBounds: Bool, cornerRadius: CGFloat)?
private let inlineNavigationBarTransition: Bool
private(set) var animatingCompletion = false
private var currentCompletion: (() -> Void)?
private var didUpdateProgress: ((CGFloat, ContainedViewLayoutTransition, CGRect, CGRect) -> Void)?
private var frameRateLink: SharedDisplayLinkDriver.Link?
init(transition: NavigationTransition, isInteractive: Bool, isFlat: Bool, container: NavigationContainer, topNode: ASDisplayNode, topNavigationBar: NavigationBar?, bottomNode: ASDisplayNode, bottomNavigationBar: NavigationBar?, screenCornerRadius: CGFloat, didUpdateProgress: ((CGFloat, ContainedViewLayoutTransition, CGRect, CGRect) -> Void)? = nil) {
self.transition = transition
self.isInteractive = isInteractive
self.isFlat = isFlat
self.container = container
self.didUpdateProgress = didUpdateProgress
self.topNode = topNode
self.bottomNode = bottomNode
self.topNavigationBar = topNavigationBar
self.bottomNavigationBar = bottomNavigationBar
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor.black
self.shadowNode = ASImageNode()
self.shadowNode.displaysAsynchronously = false
self.shadowNode.image = shadowImage
if let topNavigationBar = topNavigationBar, let bottomNavigationBar = bottomNavigationBar {
if let customTransitionNode = topNavigationBar.makeCustomTransitionNode?(bottomNavigationBar, isInteractive) {
self.inlineNavigationBarTransition = false
customTransitionNode.setup(topNavigationBar: topNavigationBar, bottomNavigationBar: bottomNavigationBar)
self.customTransitionNode = customTransitionNode
} else if let customTransitionNode = bottomNavigationBar.makeCustomTransitionNode?(topNavigationBar, isInteractive) {
self.inlineNavigationBarTransition = false
customTransitionNode.setup(topNavigationBar: topNavigationBar, bottomNavigationBar: bottomNavigationBar)
self.customTransitionNode = customTransitionNode
} else if !topNavigationBar.isHidden, !bottomNavigationBar.isHidden, topNavigationBar.canTransitionInline, bottomNavigationBar.canTransitionInline, topNavigationBar.item?.leftBarButtonItem == nil {
var topFrame = topNavigationBar.view.convert(topNavigationBar.bounds, to: container.view)
var bottomFrame = bottomNavigationBar.view.convert(bottomNavigationBar.bounds, to: container.view)
topFrame.origin.x = 0.0
bottomFrame.origin.x = 0.0
self.inlineNavigationBarTransition = true
self.customTransitionNode = nil
} else {
self.inlineNavigationBarTransition = false
self.customTransitionNode = nil
}
} else {
self.inlineNavigationBarTransition = false
self.customTransitionNode = nil
}
switch transition {
case .Push:
self.container.addSubnode(topNode)
case .Pop:
if topNode.supernode == self.container {
self.container.insertSubnode(bottomNode, belowSubnode: topNode)
} else {
self.container.addSubnode(topNode)
}
}
if !self.isFlat {
self.container.insertSubnode(self.dimNode, belowSubnode: topNode)
self.container.insertSubnode(self.shadowNode, belowSubnode: self.dimNode)
if screenCornerRadius > 0.0 {
self.topNodeInitialParameters = (topNode.clipsToBounds, topNode.cornerRadius)
if #available(iOS 13.0, *) {
topNode.layer.cornerCurve = .continuous
}
topNode.clipsToBounds = true
topNode.cornerRadius = screenCornerRadius
}
}
if let customTransitionNode = self.customTransitionNode {
self.container.addSubnode(customTransitionNode)
}
self.maybeCreateNavigationBarTransition()
self.updateProgress(0.0, transition: .immediate, completion: {})
self.frameRateLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { _ in })
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updateProgress(_ progress: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
self._progress = progress
let position: CGFloat
switch self.transition {
case .Push:
position = 1.0 - progress
case .Pop:
position = progress
}
var dimInset: CGFloat = 0.0
if let bottomNavigationBar = self.bottomNavigationBar , self.inlineNavigationBarTransition {
if self.bottomNavigationBar?.isBackgroundVisible == false || self.topNavigationBar?.isBackgroundVisible == false {
} else {
dimInset = bottomNavigationBar.frame.maxY
}
}
let containerSize = self.container.bounds.size
let topFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(position * containerSize.width), y: 0.0), size: containerSize)
let bottomFrame = CGRect(origin: CGPoint(x: self.isFlat ? -floorToScreenPixels((1.0 - position) * containerSize.width) : ((position - 1.0) * containerSize.width * 0.3), y: 0.0), size: containerSize)
var canInvokeCompletion = false
var hadEarlyCompletion = false
transition.updateFrame(node: self.topNode, frame: topFrame, completion: { _ in
if canInvokeCompletion {
completion()
} else {
hadEarlyCompletion = true
}
})
canInvokeCompletion = true
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: dimInset), size: CGSize(width: max(0.0, topFrame.minX + self.container.overflowInset + 62.0), height: self.container.bounds.size.height - dimInset)))
transition.updateFrame(node: self.shadowNode, frame: CGRect(origin: CGPoint(x: self.dimNode.frame.maxX - shadowWidth, y: dimInset), size: CGSize(width: shadowWidth, height: containerSize.height - dimInset)))
transition.updateAlpha(node: self.dimNode, alpha: (1.0 - position) * 0.15)
transition.updateAlpha(node: self.shadowNode, alpha: (1.0 - position) * 0.9)
transition.updateFrame(node: self.bottomNode, frame: bottomFrame)
self.updateNavigationBarTransition(transition: transition)
if let customTransitionNode = self.customTransitionNode {
customTransitionNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: containerSize.width, height: containerSize.height))
customTransitionNode.update(containerSize: containerSize, fraction: position, transition: transition)
}
self.didUpdateProgress?(self.progress, transition, topFrame, bottomFrame)
if hadEarlyCompletion {
completion()
}
}
private func updateNavigationBarTransition(transition: ContainedViewLayoutTransition) {
if let topNavigationBar = self.topNavigationBar, let bottomNavigationBar = self.bottomNavigationBar, self.inlineNavigationBarTransition {
let position: CGFloat
switch self.transition {
case .Push:
position = 1.0 - progress
case .Pop:
position = progress
}
transition.animateView {
topNavigationBar.transitionState = NavigationBarTransitionState(navigationBar: bottomNavigationBar, transition: self.transition, role: .top, progress: position)
bottomNavigationBar.transitionState = NavigationBarTransitionState(navigationBar: topNavigationBar, transition: self.transition, role: .bottom, progress: position)
}
}
}
func maybeCreateNavigationBarTransition() {
if let topNavigationBar = self.topNavigationBar, let bottomNavigationBar = self.bottomNavigationBar, self.inlineNavigationBarTransition {
let position: CGFloat
switch self.transition {
case .Push:
position = 1.0 - progress
case .Pop:
position = progress
}
topNavigationBar.transitionState = NavigationBarTransitionState(navigationBar: bottomNavigationBar, transition: self.transition, role: .top, progress: position)
bottomNavigationBar.transitionState = NavigationBarTransitionState(navigationBar: topNavigationBar, transition: self.transition, role: .bottom, progress: position)
}
}
func endNavigationBarTransition() {
if let topNavigationBar = self.topNavigationBar, let bottomNavigationBar = self.bottomNavigationBar, self.inlineNavigationBarTransition {
topNavigationBar.transitionState = nil
bottomNavigationBar.transitionState = nil
}
}
func animateCancel(_ completion: @escaping () -> ()) {
self.currentCompletion = completion
self.updateProgress(0.0, transition: .animated(duration: 0.1, curve: .easeInOut), completion: { [weak self] in
guard let strongSelf = self else {
return
}
switch strongSelf.transition {
case .Push:
strongSelf.topNode.removeFromSupernode()
case .Pop:
strongSelf.bottomNode.removeFromSupernode()
}
strongSelf.dimNode.removeFromSupernode()
strongSelf.shadowNode.removeFromSupernode()
strongSelf.customTransitionNode?.restore()
strongSelf.customTransitionNode?.removeFromSupernode()
strongSelf.endNavigationBarTransition()
strongSelf.restoreTopNodeCorners()
if let currentCompletion = strongSelf.currentCompletion {
strongSelf.currentCompletion = nil
currentCompletion()
}
})
}
func complete() {
self.animatingCompletion = true
self._progress = 1.0
self.dimNode.removeFromSupernode()
self.shadowNode.removeFromSupernode()
self.customTransitionNode?.restore()
self.customTransitionNode?.removeFromSupernode()
self.endNavigationBarTransition()
self.restoreTopNodeCorners()
if let currentCompletion = self.currentCompletion {
self.currentCompletion = nil
currentCompletion()
}
}
func performCompletion(completion: @escaping () -> ()) {
self.updateProgress(1.0, transition: .immediate, completion: { [weak self] in
if let strongSelf = self {
strongSelf.dimNode.removeFromSupernode()
strongSelf.shadowNode.removeFromSupernode()
strongSelf.customTransitionNode?.restore()
strongSelf.customTransitionNode?.removeFromSupernode()
strongSelf.endNavigationBarTransition()
strongSelf.restoreTopNodeCorners()
if let currentCompletion = strongSelf.currentCompletion {
strongSelf.currentCompletion = nil
currentCompletion()
}
}
completion()
})
}
func animateCompletion(_ velocity: CGFloat, completion: @escaping () -> ()) {
self.animatingCompletion = true
let distance = (1.0 - self.progress) * self.container.bounds.size.width
self.currentCompletion = completion
let f = {
self.dimNode.removeFromSupernode()
self.shadowNode.removeFromSupernode()
self.customTransitionNode?.restore()
self.customTransitionNode?.removeFromSupernode()
self.endNavigationBarTransition()
self.restoreTopNodeCorners()
if let currentCompletion = self.currentCompletion {
self.currentCompletion = nil
currentCompletion()
}
}
if abs(velocity) < CGFloat.ulpOfOne && abs(self.progress) < CGFloat.ulpOfOne {
self.updateProgress(1.0, transition: .animated(duration: 0.5, curve: .spring), completion: {
f()
})
} else {
self.updateProgress(1.0, transition: .animated(duration: Double(max(0.05, min(0.2, abs(distance / velocity)))), curve: .easeInOut), completion: {
f()
})
}
}
private func restoreTopNodeCorners() {
guard let (clipsToBounds, cornerRadius) = self.topNodeInitialParameters else {
return
}
if #available(iOS 13.0, *) {
self.topNode.layer.cornerCurve = .circular
}
self.topNode.clipsToBounds = clipsToBounds
self.topNode.cornerRadius = cornerRadius
}
}
@@ -0,0 +1,57 @@
import Foundation
import UIKit
import AsyncDisplayKit
open class ASImageNode: ASDisplayNode {
public var image: UIImage? {
didSet {
if self.isNodeLoaded {
if let image = self.image {
let capInsets = image.capInsets
if capInsets.left.isZero && capInsets.top.isZero && capInsets.right.isZero && capInsets.bottom.isZero {
self.contentsScale = image.scale
self.contents = image.cgImage
} else {
ASDisplayNodeSetResizableContents(self.layer, image)
}
} else {
self.contents = nil
}
if self.image?.size != oldValue?.size {
self.invalidateCalculatedLayout()
}
}
}
}
public var customTintColor: UIColor? {
didSet {
self.layer.layerTintColor = self.customTintColor?.cgColor
}
}
public var displayWithoutProcessing: Bool = true
override public init() {
super.init()
}
override open func didLoad() {
super.didLoad()
if let image = self.image {
let capInsets = image.capInsets
if capInsets.left.isZero && capInsets.top.isZero {
self.contentsScale = image.scale
self.contents = image.cgImage
} else {
ASDisplayNodeSetResizableContents(self.layer, image)
}
}
self.layer.layerTintColor = self.customTintColor?.cgColor
}
override public func calculateSizeThatFits(_ contrainedSize: CGSize) -> CGSize {
return self.image?.size ?? CGSize()
}
}
@@ -0,0 +1,443 @@
import Foundation
import UIKit
import AsyncDisplayKit
open class ASButtonNode: ASControlNode {
public let titleNode: ImmediateTextNode
public let highlightedTitleNode: ImmediateTextNode
public let disabledTitleNode: ImmediateTextNode
public let imageNode: ASImageNode
public let highlightedImageNode: ASImageNode
public let selectedImageNode: ASImageNode
public let highlightedSelectedImageNode: ASImageNode
public let disabledImageNode: ASImageNode
public let backgroundImageNode: ASImageNode
public let highlightedBackgroundImageNode: ASImageNode
public var contentEdgeInsets: UIEdgeInsets = UIEdgeInsets() {
didSet {
if self.contentEdgeInsets != oldValue {
self.invalidateCalculatedLayout()
self.setNeedsLayout()
}
}
}
public var contentHorizontalAlignment: ASHorizontalAlignment = .middle {
didSet {
if self.contentHorizontalAlignment != oldValue {
self.invalidateCalculatedLayout()
self.setNeedsLayout()
}
}
}
public var laysOutHorizontally: Bool = true {
didSet {
if self.laysOutHorizontally != oldValue {
self.invalidateCalculatedLayout()
self.setNeedsLayout()
}
}
}
public var contentSpacing: CGFloat = 0.0 {
didSet {
if self.contentSpacing != oldValue {
self.invalidateCalculatedLayout()
self.setNeedsLayout()
}
}
}
private var calculatedTitleSize: CGSize = CGSize()
private var calculatedHighlightedTitleSize: CGSize = CGSize()
private var calculatedDisabledTitleSize: CGSize = CGSize()
override public init() {
self.titleNode = ImmediateTextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = false
self.highlightedTitleNode = ImmediateTextNode()
self.highlightedTitleNode.isUserInteractionEnabled = false
self.highlightedTitleNode.displaysAsynchronously = false
self.disabledTitleNode = ImmediateTextNode()
self.disabledTitleNode.isUserInteractionEnabled = false
self.disabledTitleNode.displaysAsynchronously = false
self.imageNode = ASImageNode()
self.imageNode.isUserInteractionEnabled = false
self.imageNode.displaysAsynchronously = false
self.imageNode.displayWithoutProcessing = true
self.selectedImageNode = ASImageNode()
self.selectedImageNode.isUserInteractionEnabled = false
self.selectedImageNode.displaysAsynchronously = false
self.selectedImageNode.displayWithoutProcessing = true
self.highlightedImageNode = ASImageNode()
self.highlightedImageNode.isUserInteractionEnabled = false
self.highlightedImageNode.displaysAsynchronously = false
self.highlightedImageNode.displayWithoutProcessing = true
self.highlightedSelectedImageNode = ASImageNode()
self.highlightedSelectedImageNode.isUserInteractionEnabled = false
self.highlightedSelectedImageNode.displaysAsynchronously = false
self.highlightedSelectedImageNode.displayWithoutProcessing = true
self.disabledImageNode = ASImageNode()
self.disabledImageNode.isUserInteractionEnabled = false
self.disabledImageNode.displaysAsynchronously = false
self.disabledImageNode.displayWithoutProcessing = true
self.backgroundImageNode = ASImageNode()
self.backgroundImageNode.isUserInteractionEnabled = false
self.backgroundImageNode.displaysAsynchronously = false
self.backgroundImageNode.displayWithoutProcessing = true
self.highlightedBackgroundImageNode = ASImageNode()
self.highlightedBackgroundImageNode.isUserInteractionEnabled = false
self.highlightedBackgroundImageNode.displaysAsynchronously = false
self.highlightedBackgroundImageNode.displayWithoutProcessing = true
super.init()
self.addSubnode(self.backgroundImageNode)
self.addSubnode(self.highlightedBackgroundImageNode)
self.highlightedBackgroundImageNode.isHidden = true
self.addSubnode(self.titleNode)
self.addSubnode(self.highlightedTitleNode)
self.highlightedTitleNode.isHidden = true
self.addSubnode(self.disabledTitleNode)
self.disabledTitleNode.isHidden = true
self.addSubnode(self.imageNode)
self.addSubnode(self.selectedImageNode)
self.selectedImageNode.isHidden = true
self.addSubnode(self.highlightedImageNode)
self.highlightedImageNode.isHidden = true
self.addSubnode(self.highlightedSelectedImageNode)
self.highlightedSelectedImageNode.isHidden = true
self.addSubnode(self.disabledImageNode)
self.disabledImageNode.isHidden = true
}
override open func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
let horizontalInsets = self.contentEdgeInsets.left + self.contentEdgeInsets.right
let verticalInsets = self.contentEdgeInsets.top + self.contentEdgeInsets.bottom
let imageSize = self.imageNode.image?.size ?? CGSize()
let widthForTitle: CGFloat
if self.laysOutHorizontally {
widthForTitle = max(1.0, constrainedSize.width - horizontalInsets - imageSize.width - (imageSize.width.isZero ? 0.0 : self.contentSpacing))
} else {
widthForTitle = max(1.0, constrainedSize.width - horizontalInsets)
}
let normalTitleSize = self.titleNode.updateLayout(CGSize(width: widthForTitle, height: max(1.0, constrainedSize.height - verticalInsets)))
self.calculatedTitleSize = normalTitleSize
let highlightedTitleSize = self.highlightedTitleNode.updateLayout(CGSize(width: widthForTitle, height: max(1.0, constrainedSize.height - verticalInsets)))
self.calculatedHighlightedTitleSize = highlightedTitleSize
self.calculatedDisabledTitleSize = self.disabledTitleNode.updateLayout(CGSize(width: widthForTitle, height: max(1.0, constrainedSize.height - verticalInsets)))
let titleSize = CGSize(width: max(normalTitleSize.width, highlightedTitleSize.width), height: max(normalTitleSize.height, highlightedTitleSize.height))
var contentSize: CGSize
if self.laysOutHorizontally {
contentSize = CGSize(width: titleSize.width + imageSize.width, height: max(titleSize.height, imageSize.height))
if !titleSize.width.isZero && !imageSize.width.isZero {
contentSize.width += self.contentSpacing
}
} else {
contentSize = CGSize(width: max(titleSize.width, imageSize.width), height: titleSize.height + imageSize.height)
if !titleSize.width.isZero && !imageSize.width.isZero {
contentSize.height += self.contentSpacing
}
}
return CGSize(width: min(constrainedSize.width, contentSize.width + self.contentEdgeInsets.left + self.contentEdgeInsets.right), height: min(constrainedSize.height, contentSize.height + self.contentEdgeInsets.top + self.contentEdgeInsets.bottom))
}
open func setAttributedTitle(_ title: NSAttributedString, for state: UIControl.State) {
if state == [] {
if let attributedText = self.titleNode.attributedText {
if !attributedText.isEqual(to: title) {
self.invalidateCalculatedLayout()
self.setNeedsLayout()
}
} else {
self.invalidateCalculatedLayout()
self.setNeedsLayout()
}
self.titleNode.attributedText = title
if let attributedText = self.highlightedTitleNode.attributedText {
if !attributedText.isEqual(to: title) {
self.invalidateCalculatedLayout()
self.setNeedsLayout()
}
} else {
self.invalidateCalculatedLayout()
self.setNeedsLayout()
}
self.highlightedTitleNode.attributedText = title
} else if state == .highlighted || state == .selected {
if let attributedText = self.highlightedTitleNode.attributedText {
if !attributedText.isEqual(to: title) {
self.invalidateCalculatedLayout()
self.setNeedsLayout()
}
} else {
self.invalidateCalculatedLayout()
self.setNeedsLayout()
}
self.highlightedTitleNode.attributedText = title
} else if state == .disabled {
if let attributedText = self.disabledTitleNode.attributedText {
if !attributedText.isEqual(to: title) {
self.invalidateCalculatedLayout()
self.setNeedsLayout()
}
} else {
self.invalidateCalculatedLayout()
self.setNeedsLayout()
}
self.disabledTitleNode.attributedText = title
} else {
if let attributedText = self.titleNode.attributedText {
if !attributedText.isEqual(to: title) {
self.invalidateCalculatedLayout()
self.setNeedsLayout()
}
} else {
self.invalidateCalculatedLayout()
self.setNeedsLayout()
}
self.titleNode.attributedText = title
}
}
open func attributedTitle(for state: UIControl.State) -> NSAttributedString? {
if state == .highlighted || state == .selected {
return self.highlightedTitleNode.attributedText
} else if state == .disabled {
return self.disabledTitleNode.attributedText
} else {
return self.titleNode.attributedText
}
}
open func setTitle(_ title: String, with font: UIFont, with color: UIColor, for state: UIControl.State) {
self.setAttributedTitle(NSAttributedString(string: title, font: font, textColor: color), for: state)
}
open func setImage(_ image: UIImage?, for state: UIControl.State) {
if image?.size != self.imageNode.image?.size {
self.invalidateCalculatedLayout()
self.setNeedsLayout()
}
if state == .disabled {
self.disabledImageNode.image = image
} else if state == [] {
self.imageNode.image = image
} else if state == .highlighted {
self.highlightedImageNode.image = image
} else if state == .selected {
self.selectedImageNode.image = image
} else if state == [.selected, .highlighted] {
self.highlightedSelectedImageNode.image = image
} else {
self.imageNode.image = image
}
}
open func setBackgroundImage(_ image: UIImage?, for state: UIControl.State) {
if state == [] {
self.backgroundImageNode.image = image
self.highlightedBackgroundImageNode.image = image
} else if state == .highlighted || state == .selected || state == [.selected, .highlighted] {
self.highlightedBackgroundImageNode.image = image
} else {
self.backgroundImageNode.image = image
}
}
open func image(for state: UIControl.State) -> UIImage? {
switch state {
case .disabled:
return self.disabledImageNode.image ?? self.imageNode.image
default:
return self.imageNode.image
}
}
open func backgroundImage(for state: UIControl.State) -> UIImage? {
return self.backgroundImageNode.image
}
override open var isSelected: Bool {
didSet {
if self.isSelected != oldValue {
if self.isSelected {
if self.selectedImageNode.image != nil {
self.selectedImageNode.isHidden = false
self.imageNode.isHidden = true
} else {
self.selectedImageNode.isHidden = true
self.imageNode.isHidden = false
}
} else {
self.selectedImageNode.isHidden = true
self.imageNode.isHidden = false
}
}
}
}
override open var isHighlighted: Bool {
didSet {
if self.isHighlighted != oldValue && !self.isImplicitlyDisabled {
let isHighlighted = self.isHighlighted
if isHighlighted {
if self.highlightedTitleNode.attributedText != nil {
self.highlightedTitleNode.isHidden = false
self.titleNode.isHidden = true
} else {
self.highlightedTitleNode.isHidden = true
self.titleNode.isHidden = false
}
if self.highlightedBackgroundImageNode.image != nil {
self.highlightedBackgroundImageNode.isHidden = false
self.backgroundImageNode.isHidden = true
} else {
self.highlightedBackgroundImageNode.isHidden = true
self.backgroundImageNode.isHidden = false
}
if self.isSelected && self.highlightedSelectedImageNode.image != nil {
self.highlightedSelectedImageNode.isHidden = false
self.highlightedImageNode.isHidden = true
self.selectedImageNode.isHidden = true
self.imageNode.isHidden = true
} else if self.highlightedImageNode.image != nil {
self.highlightedSelectedImageNode.isHidden = true
self.highlightedImageNode.isHidden = false
self.imageNode.isHidden = true
} else {
self.highlightedSelectedImageNode.isHidden = true
self.highlightedImageNode.isHidden = true
self.imageNode.isHidden = false
}
} else {
self.highlightedTitleNode.isHidden = true
self.titleNode.isHidden = false
self.highlightedBackgroundImageNode.isHidden = true
self.backgroundImageNode.isHidden = false
self.highlightedSelectedImageNode.isHidden = true
self.highlightedImageNode.isHidden = true
if self.isSelected && self.selectedImageNode.image != nil {
self.selectedImageNode.isHidden = false
self.imageNode.isHidden = true
} else {
self.selectedImageNode.isHidden = true
self.imageNode.isHidden = false
}
}
}
}
}
open var isImplicitlyDisabled: Bool = false {
didSet {
if self.isImplicitlyDisabled != oldValue {
self.updateIsEnabled()
}
}
}
override open var isEnabled: Bool {
didSet {
if self.isEnabled != oldValue {
self.updateIsEnabled()
}
}
}
private func updateIsEnabled() {
let isEnabled = self.isEnabled && !self.isImplicitlyDisabled
if isEnabled || self.disabledTitleNode.attributedText == nil {
self.titleNode.isHidden = false
self.disabledTitleNode.isHidden = true
} else {
self.titleNode.isHidden = true
self.disabledTitleNode.isHidden = false
}
if isEnabled || self.disabledImageNode.image == nil {
self.imageNode.isHidden = false
self.disabledImageNode.isHidden = true
} else {
self.imageNode.isHidden = true
self.disabledImageNode.isHidden = false
}
}
override open func layout() {
let size = self.bounds.size
let contentRect = CGRect(origin: CGPoint(x: self.contentEdgeInsets.left, y: self.contentEdgeInsets.top), size: CGSize(width: size.width - self.contentEdgeInsets.left - self.contentEdgeInsets.right, height: size.height - self.contentEdgeInsets.top - self.contentEdgeInsets.bottom))
let imageSize = self.imageNode.image?.size ?? CGSize()
let titleOrigin: CGPoint
let highlightedTitleOrigin: CGPoint
let disabledTitleOrigin: CGPoint
let imageOrigin: CGPoint
if self.laysOutHorizontally {
switch self.contentHorizontalAlignment {
case .left:
titleOrigin = CGPoint(x: contentRect.minX, y: contentRect.minY + floorToScreenPixels((contentRect.height - self.calculatedTitleSize.height) / 2.0))
highlightedTitleOrigin = CGPoint(x: contentRect.minX, y: contentRect.minY + floorToScreenPixels((contentRect.height - self.calculatedHighlightedTitleSize.height) / 2.0))
disabledTitleOrigin = CGPoint(x: contentRect.minX, y: contentRect.minY + floorToScreenPixels((contentRect.height - self.calculatedDisabledTitleSize.height) / 2.0))
imageOrigin = CGPoint(x: titleOrigin.x + self.calculatedTitleSize.width + self.contentSpacing, y: contentRect.minY + floorToScreenPixels((contentRect.height - imageSize.height) / 2.0))
case .right:
titleOrigin = CGPoint(x: contentRect.maxX - self.calculatedTitleSize.width, y: contentRect.minY + floorToScreenPixels((contentRect.height - self.calculatedTitleSize.height) / 2.0))
highlightedTitleOrigin = CGPoint(x: contentRect.maxX - self.calculatedHighlightedTitleSize.width, y: contentRect.minY + floorToScreenPixels((contentRect.height - self.calculatedHighlightedTitleSize.height) / 2.0))
disabledTitleOrigin = CGPoint(x: contentRect.maxX - self.calculatedDisabledTitleSize.width, y: contentRect.minY + floorToScreenPixels((contentRect.height - self.calculatedDisabledTitleSize.height) / 2.0))
imageOrigin = CGPoint(x: titleOrigin.x - self.contentSpacing - imageSize.width, y: contentRect.minY + floorToScreenPixels((contentRect.height - imageSize.height) / 2.0))
default:
titleOrigin = CGPoint(x: contentRect.minX + floorToScreenPixels((contentRect.width - self.calculatedTitleSize.width) / 2.0), y: contentRect.minY + floorToScreenPixels((contentRect.height - self.calculatedTitleSize.height) / 2.0))
highlightedTitleOrigin = CGPoint(x: contentRect.minX + floorToScreenPixels((contentRect.width - self.calculatedHighlightedTitleSize.width) / 2.0), y: contentRect.minY + floorToScreenPixels((contentRect.height - self.calculatedHighlightedTitleSize.height) / 2.0))
disabledTitleOrigin = CGPoint(x: floorToScreenPixels((contentRect.width - self.calculatedDisabledTitleSize.width) / 2.0), y: contentRect.minY + floorToScreenPixels((contentRect.height - self.calculatedDisabledTitleSize.height) / 2.0))
imageOrigin = CGPoint(x: floorToScreenPixels((contentRect.width - imageSize.width) / 2.0), y: contentRect.minY + floorToScreenPixels((contentRect.height - imageSize.height) / 2.0))
}
} else {
var contentHeight: CGFloat = self.calculatedTitleSize.height
if !imageSize.height.isZero {
contentHeight += self.contentSpacing + imageSize.height
}
let contentY = contentRect.minY + floorToScreenPixels((contentRect.height - contentHeight) / 2.0)
titleOrigin = CGPoint(x: contentRect.minX + floorToScreenPixels((contentRect.width - self.calculatedTitleSize.width) / 2.0), y: contentY + contentHeight - self.calculatedTitleSize.height)
highlightedTitleOrigin = CGPoint(x: contentRect.minX + floorToScreenPixels((contentRect.width - self.calculatedHighlightedTitleSize.width) / 2.0), y: contentY + contentHeight - self.calculatedHighlightedTitleSize.height)
disabledTitleOrigin = CGPoint(x: contentRect.minX + floorToScreenPixels((contentRect.width - self.calculatedDisabledTitleSize.width) / 2.0), y: contentY + contentHeight - self.calculatedDisabledTitleSize.height)
imageOrigin = CGPoint(x: floorToScreenPixels((contentRect.width - imageSize.width) / 2.0), y: contentY)
}
self.titleNode.frame = CGRect(origin: titleOrigin, size: self.calculatedTitleSize)
self.highlightedTitleNode.frame = CGRect(origin: highlightedTitleOrigin, size: self.calculatedHighlightedTitleSize)
self.disabledTitleNode.frame = CGRect(origin: disabledTitleOrigin, size: self.calculatedDisabledTitleSize)
self.imageNode.frame = CGRect(origin: imageOrigin, size: imageSize)
self.selectedImageNode.frame = CGRect(origin: imageOrigin, size: imageSize)
self.highlightedImageNode.frame = CGRect(origin: imageOrigin, size: imageSize)
self.highlightedSelectedImageNode.frame = CGRect(origin: imageOrigin, size: imageSize)
self.disabledImageNode.frame = CGRect(origin: imageOrigin, size: imageSize)
self.backgroundImageNode.frame = CGRect(origin: CGPoint(), size: size)
self.highlightedBackgroundImageNode.frame = CGRect(origin: CGPoint(), size: size)
}
}

Some files were not shown because too many files have changed in this diff Show More