Add 'submodules/Display/' from commit '7bd11013ea936e3d49d937550d599f5816d32560'

git-subtree-dir: submodules/Display
git-subtree-mainline: 9bc996374ffdad37aef175427db72731c9551dcf
git-subtree-split: 7bd11013ea936e3d49d937550d599f5816d32560
This commit is contained in:
Peter 2019-06-11 18:44:37 +01:00
commit 8f5a4f7dc1
163 changed files with 30208 additions and 0 deletions

25
submodules/Display/.gitignore vendored Normal file
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/*

0
submodules/Display/.gitmodules vendored Normal file
View File

53
submodules/Display/BUCK Normal file
View File

@ -0,0 +1,53 @@
load('//tools:buck_utils.bzl', 'config_with_updated_linker_flags', 'configs_with_config', 'combined_config')
load('//tools:buck_defs.bzl', 'SHARED_CONFIGS', 'EXTENSION_LIB_SPECIFIC_CONFIG')
apple_library(
name = 'DisplayPrivate',
srcs = glob([
'Display/*.m',
]),
headers = glob([
'Display/*.h',
]),
header_namespace = 'DisplayPrivate',
exported_headers = glob([
'Display/*.h',
], exclude = ['Display/Display.h']),
modular = True,
configs = configs_with_config(combined_config([SHARED_CONFIGS, EXTENSION_LIB_SPECIFIC_CONFIG])),
compiler_flags = ['-w'],
preprocessor_flags = ['-fobjc-arc'],
visibility = ['//submodules/Display:Display'],
deps = [
'//submodules/AsyncDisplayKit:AsyncDisplayKit',
],
frameworks = [
'$SDKROOT/System/Library/Frameworks/Foundation.framework',
'$SDKROOT/System/Library/Frameworks/UIKit.framework',
],
)
apple_library(
name = 'Display',
srcs = glob([
'Display/*.swift',
]),
configs = configs_with_config(combined_config([SHARED_CONFIGS, EXTENSION_LIB_SPECIFIC_CONFIG])),
swift_compiler_flags = [
'-suppress-warnings',
'-application-extension',
],
visibility = ['PUBLIC'],
deps = [
':DisplayPrivate',
'//submodules/AsyncDisplayKit:AsyncDisplayKit',
'//submodules/SSignalKit:SwiftSignalKit',
],
frameworks = [
'$SDKROOT/System/Library/Frameworks/Foundation.framework',
'$SDKROOT/System/Library/Frameworks/UIKit.framework',
'$SDKROOT/System/Library/Frameworks/QuartzCore.framework',
'$SDKROOT/System/Library/Frameworks/CoreText.framework',
'$SDKROOT/System/Library/Frameworks/CoreGraphics.framework',
],
)

View File

@ -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()
})
}
}
}

View File

@ -0,0 +1,21 @@
import Foundation
import UIKit
import AsyncDisplayKit
public func addAccessibilityChildren(of node: ASDisplayNode, container: Any, to list: inout [Any]) {
if node.isAccessibilityElement {
let element = UIAccessibilityElement(accessibilityContainer: container)
element.accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates(node.bounds, node.view)
element.accessibilityLabel = node.accessibilityLabel
element.accessibilityValue = node.accessibilityValue
element.accessibilityTraits = node.accessibilityTraits
element.accessibilityHint = node.accessibilityHint
element.accessibilityIdentifier = node.accessibilityIdentifier
//node.accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates(node.bounds, node.view)
list.append(element)
} else if let accessibilityElements = node.accessibilityElements {
list.append(contentsOf: accessibilityElements)
}
}

View File

@ -0,0 +1,21 @@
import Foundation
import UIKit
import AsyncDisplayKit
public final class AccessibilityAreaNode: ASDisplayNode {
public var activate: (() -> Bool)?
override public init() {
super.init()
self.isAccessibilityElement = true
}
override public func accessibilityActivate() -> Bool {
return self.activate?() ?? false
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return nil
}
}

View File

@ -0,0 +1,138 @@
import Foundation
import UIKit
import AsyncDisplayKit
public enum ActionSheetButtonColor {
case accent
case destructive
case disabled
}
public enum ActionSheetButtonFont {
case `default`
case bold
}
public class ActionSheetButtonItem: ActionSheetItem {
public let title: String
public let color: ActionSheetButtonColor
public let font: ActionSheetButtonFont
public let enabled: Bool
public let action: () -> Void
public init(title: String, color: ActionSheetButtonColor = .accent, font: ActionSheetButtonFont = .default, enabled: Bool = true, action: @escaping () -> Void) {
self.title = title
self.color = color
self.font = font
self.enabled = enabled
self.action = action
}
public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
let node = ActionSheetButtonNode(theme: theme)
node.setItem(self)
return node
}
public func updateNode(_ node: ActionSheetItemNode) {
guard let node = node as? ActionSheetButtonNode else {
assertionFailure()
return
}
node.setItem(self)
}
}
public class ActionSheetButtonNode: ActionSheetItemNode {
private let theme: ActionSheetControllerTheme
public static let defaultFont: UIFont = Font.regular(20.0)
public static let boldFont: UIFont = Font.medium(20.0)
private var item: ActionSheetButtonItem?
private let button: HighlightTrackingButton
private let label: ASTextNode
override public init(theme: ActionSheetControllerTheme) {
self.theme = theme
self.button = HighlightTrackingButton()
self.label = ASTextNode()
self.label.isUserInteractionEnabled = false
self.label.maximumNumberOfLines = 1
self.label.displaysAsynchronously = false
self.label.truncationMode = .byTruncatingTail
super.init(theme: theme)
self.view.addSubview(self.button)
self.label.isUserInteractionEnabled = false
self.addSubnode(self.label)
self.button.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemHighlightedBackgroundColor
} else {
UIView.animate(withDuration: 0.3, animations: {
strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemBackgroundColor
})
}
}
}
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
}
func setItem(_ item: ActionSheetButtonItem) {
self.item = item
let textColor: UIColor
let textFont: UIFont
switch item.color {
case .accent:
textColor = self.theme.standardActionTextColor
case .destructive:
textColor = self.theme.destructiveActionTextColor
case .disabled:
textColor = self.theme.disabledActionTextColor
}
switch item.font {
case .default:
textFont = ActionSheetButtonNode.defaultFont
case .bold:
textFont = ActionSheetButtonNode.boldFont
}
self.label.attributedText = NSAttributedString(string: item.title, font: textFont, textColor: textColor)
self.button.isEnabled = item.enabled
self.setNeedsLayout()
}
public override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
return CGSize(width: constrainedSize.width, height: 57.0)
}
public override func layout() {
super.layout()
let size = self.bounds.size
self.button.frame = CGRect(origin: CGPoint(), size: size)
let labelSize = self.label.measure(CGSize(width: max(1.0, size.width - 10.0), height: size.height))
self.label.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - labelSize.width) / 2.0), y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize)
}
@objc func buttonPressed() {
if let item = self.item {
item.action()
}
}
}

View File

@ -0,0 +1,147 @@
import Foundation
import UIKit
import AsyncDisplayKit
public enum ActionSheetCheckboxStyle {
case `default`
case alignRight
}
public class ActionSheetCheckboxItem: ActionSheetItem {
public let title: String
public let label: String
public let value: Bool
public let style: ActionSheetCheckboxStyle
public let action: (Bool) -> Void
public init(title: String, label: String, value: Bool, style: ActionSheetCheckboxStyle = .default, action: @escaping (Bool) -> Void) {
self.title = title
self.label = label
self.value = value
self.style = style
self.action = action
}
public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
let node = ActionSheetCheckboxItemNode(theme: theme)
node.setItem(self)
return node
}
public func updateNode(_ node: ActionSheetItemNode) {
guard let node = node as? ActionSheetCheckboxItemNode else {
assertionFailure()
return
}
node.setItem(self)
}
}
public class ActionSheetCheckboxItemNode: ActionSheetItemNode {
public static let defaultFont: UIFont = Font.regular(20.0)
private let theme: ActionSheetControllerTheme
private var item: ActionSheetCheckboxItem?
private let button: HighlightTrackingButton
private let titleNode: ImmediateTextNode
private let labelNode: ImmediateTextNode
private let checkNode: ASImageNode
override public init(theme: ActionSheetControllerTheme) {
self.theme = theme
self.button = HighlightTrackingButton()
self.titleNode = ImmediateTextNode()
self.titleNode.maximumNumberOfLines = 1
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = false
self.labelNode = ImmediateTextNode()
self.labelNode.maximumNumberOfLines = 1
self.labelNode.isUserInteractionEnabled = false
self.labelNode.displaysAsynchronously = false
self.checkNode = ASImageNode()
self.checkNode.isUserInteractionEnabled = false
self.checkNode.displayWithoutProcessing = true
self.checkNode.displaysAsynchronously = false
self.checkNode.image = generateImage(CGSize(width: 14.0, height: 11.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(theme.controlAccentColor.cgColor)
context.setLineWidth(2.0)
context.move(to: CGPoint(x: 12.0, y: 1.0))
context.addLine(to: CGPoint(x: 4.16482734, y: 9.0))
context.addLine(to: CGPoint(x: 1.0, y: 5.81145833))
context.strokePath()
})
super.init(theme: theme)
self.view.addSubview(self.button)
self.addSubnode(self.titleNode)
self.addSubnode(self.labelNode)
self.addSubnode(self.checkNode)
self.button.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemHighlightedBackgroundColor
} else {
UIView.animate(withDuration: 0.3, animations: {
strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemBackgroundColor
})
}
}
}
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
}
func setItem(_ item: ActionSheetCheckboxItem) {
self.item = item
self.titleNode.attributedText = NSAttributedString(string: item.title, font: ActionSheetCheckboxItemNode.defaultFont, textColor: self.theme.primaryTextColor)
self.labelNode.attributedText = NSAttributedString(string: item.label, font: ActionSheetCheckboxItemNode.defaultFont, textColor: self.theme.secondaryTextColor)
self.checkNode.isHidden = !item.value
self.setNeedsLayout()
}
public override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
return CGSize(width: constrainedSize.width, height: 57.0)
}
public override func layout() {
super.layout()
let size = self.bounds.size
self.button.frame = CGRect(origin: CGPoint(), size: size)
var titleOrigin: CGFloat = 44.0
var checkOrigin: CGFloat = 22.0
if let item = self.item, item.style == .alignRight {
titleOrigin = 24.0
checkOrigin = size.width - 22.0
}
let labelSize = self.labelNode.updateLayout(CGSize(width: size.width - 44.0 - 15.0 - 8.0, height: size.height))
let titleSize = self.titleNode.updateLayout(CGSize(width: size.width - 44.0 - labelSize.width - 15.0 - 8.0, height: size.height))
self.titleNode.frame = CGRect(origin: CGPoint(x: titleOrigin, y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize)
self.labelNode.frame = CGRect(origin: CGPoint(x: size.width - 15.0 - labelSize.width, y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize)
if let image = self.checkNode.image {
self.checkNode.frame = CGRect(origin: CGPoint(x: floor(checkOrigin - (image.size.width / 2.0)), y: floor((size.height - image.size.height) / 2.0)), size: image.size)
}
}
@objc func buttonPressed() {
if let item = self.item {
item.action(!item.value)
}
}
}

View File

@ -0,0 +1,82 @@
import Foundation
import UIKit
open class ActionSheetController: ViewController, PresentableController {
private var actionSheetNode: ActionSheetControllerNode {
return self.displayNode as! ActionSheetControllerNode
}
public var theme: ActionSheetControllerTheme {
didSet {
if oldValue != self.theme {
self.actionSheetNode.theme = self.theme
}
}
}
private var groups: [ActionSheetItemGroup] = []
private var isDismissed: Bool = false
public var dismissed: ((Bool) -> Void)?
public init(theme: ActionSheetControllerTheme) {
self.theme = theme
super.init(navigationBarPresentationData: nil)
self.blocksBackgroundWhenInOverlay = true
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func dismissAnimated() {
if !self.isDismissed {
self.isDismissed = true
self.actionSheetNode.animateOut(cancelled: false)
}
}
open override func loadDisplayNode() {
self.displayNode = ActionSheetControllerNode(theme: self.theme)
self.displayNodeDidLoad()
self.actionSheetNode.dismiss = { [weak self] cancelled in
self?.dismissed?(cancelled)
self?.presentingViewController?.dismiss(animated: false)
}
self.actionSheetNode.setGroups(self.groups)
}
override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.actionSheetNode.containerLayoutUpdated(layout, transition: transition)
}
open override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.viewDidAppear(completion: {})
}
public func viewDidAppear(completion: @escaping () -> Void) {
self.actionSheetNode.animateIn(completion: completion)
}
public func setItemGroups(_ groups: [ActionSheetItemGroup]) {
self.groups = groups
if self.isViewLoaded {
self.actionSheetNode.setGroups(groups)
}
}
public func updateItem(groupIndex: Int, itemIndex: Int, _ f: (ActionSheetItem) -> ActionSheetItem) {
if self.isViewLoaded {
self.actionSheetNode.updateItem(groupIndex: groupIndex, itemIndex: itemIndex, f)
}
}
}

View File

@ -0,0 +1,200 @@
import UIKit
import AsyncDisplayKit
private let containerInsets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
private class ActionSheetControllerNodeScrollView: UIScrollView {
override func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
}
final class ActionSheetControllerNode: ASDisplayNode, UIScrollViewDelegate {
var theme: ActionSheetControllerTheme {
didSet {
self.itemGroupsContainerNode.theme = self.theme
self.updateTheme()
}
}
private let dismissTapView: UIView
private let leftDimView: UIView
private let rightDimView: UIView
private let topDimView: UIView
private let bottomDimView: UIView
private let itemGroupsContainerNode: ActionSheetItemGroupsContainerNode
private let scrollView: UIScrollView
var dismiss: (Bool) -> Void = { _ in }
private var validLayout: ContainerViewLayout?
init(theme: ActionSheetControllerTheme) {
self.theme = theme
self.scrollView = ActionSheetControllerNodeScrollView()
if #available(iOSApplicationExtension 11.0, *) {
self.scrollView.contentInsetAdjustmentBehavior = .never
}
self.scrollView.alwaysBounceVertical = true
self.scrollView.delaysContentTouches = false
self.scrollView.canCancelContentTouches = true
self.dismissTapView = UIView()
self.leftDimView = UIView()
self.leftDimView.isUserInteractionEnabled = false
self.rightDimView = UIView()
self.rightDimView.isUserInteractionEnabled = false
self.topDimView = UIView()
self.topDimView.isUserInteractionEnabled = false
self.bottomDimView = UIView()
self.bottomDimView.isUserInteractionEnabled = false
self.itemGroupsContainerNode = ActionSheetItemGroupsContainerNode(theme: self.theme)
super.init()
self.scrollView.delegate = self
self.view.addSubview(self.scrollView)
self.scrollView.addSubview(self.dismissTapView)
self.scrollView.addSubview(self.leftDimView)
self.scrollView.addSubview(self.rightDimView)
self.scrollView.addSubview(self.topDimView)
self.scrollView.addSubview(self.bottomDimView)
self.dismissTapView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimNodeTap(_:))))
self.scrollView.addSubnode(self.itemGroupsContainerNode)
self.updateTheme()
}
func updateTheme() {
self.leftDimView.backgroundColor = self.theme.dimColor
self.rightDimView.backgroundColor = self.theme.dimColor
self.topDimView.backgroundColor = self.theme.dimColor
self.bottomDimView.backgroundColor = self.theme.dimColor
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
var insets = layout.insets(options: [.statusBar])
let containerWidth = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left)
insets.left = floor((layout.size.width - containerWidth) / 2.0)
insets.right = insets.left
if !insets.bottom.isZero {
insets.bottom -= 12.0
}
self.validLayout = layout
self.scrollView.frame = CGRect(origin: CGPoint(), size: layout.size)
self.dismissTapView.frame = CGRect(origin: CGPoint(), size: layout.size)
self.itemGroupsContainerNode.measure(CGSize(width: layout.size.width - containerInsets.left - containerInsets.right - insets.left - insets.right, height: layout.size.height - containerInsets.top - containerInsets.bottom - insets.top - insets.bottom))
self.itemGroupsContainerNode.frame = CGRect(origin: CGPoint(x: insets.left + containerInsets.left, y: layout.size.height - insets.bottom - containerInsets.bottom - self.itemGroupsContainerNode.calculatedSize.height), size: self.itemGroupsContainerNode.calculatedSize)
self.itemGroupsContainerNode.layout()
self.updateScrollDimViews(size: layout.size, insets: insets)
}
func animateIn(completion: @escaping () -> Void) {
let tempDimView = UIView()
tempDimView.backgroundColor = self.theme.dimColor
tempDimView.frame = self.bounds.offsetBy(dx: 0.0, dy: -self.bounds.size.height)
self.view.addSubview(tempDimView)
for node in [tempDimView, self.topDimView, self.leftDimView, self.rightDimView, self.bottomDimView] {
node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
self.itemGroupsContainerNode.animateDimViewsAlpha(from: 0.0, to: 1.0, duration: 0.4)
self.layer.animateBounds(from: self.bounds.offsetBy(dx: 0.0, dy: -self.bounds.size.height), to: self.bounds, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak tempDimView] _ in
tempDimView?.removeFromSuperview()
completion()
})
}
func animateOut(cancelled: Bool) {
let tempDimView = UIView()
tempDimView.backgroundColor = self.theme.dimColor
tempDimView.frame = self.bounds.offsetBy(dx: 0.0, dy: -self.bounds.size.height)
self.view.addSubview(tempDimView)
for node in [tempDimView, self.topDimView, self.leftDimView, self.rightDimView, self.bottomDimView] {
node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
}
self.itemGroupsContainerNode.animateDimViewsAlpha(from: 1.0, to: 0.0, duration: 0.3)
self.layer.animateBounds(from: self.bounds, to: self.bounds.offsetBy(dx: 0.0, dy: -self.bounds.size.height), duration: 0.35, timingFunction: kCAMediaTimingFunctionEaseOut, removeOnCompletion: false, completion: { [weak self, weak tempDimView] _ in
tempDimView?.removeFromSuperview()
self?.dismiss(cancelled)
})
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
return result
}
@objc func dimNodeTap(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.animateOut(cancelled: true)
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if let layout = self.validLayout {
var insets = layout.insets(options: [.statusBar])
let containerWidth = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left)
insets.left = floor((layout.size.width - containerWidth) / 2.0)
insets.right = insets.left
self.updateScrollDimViews(size: layout.size, insets: insets)
}
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
let contentOffset = self.scrollView.contentOffset
let additionalTopHeight = max(0.0, -contentOffset.y)
if additionalTopHeight >= 30.0 {
self.animateOut(cancelled: true)
}
}
func updateScrollDimViews(size: CGSize, insets: UIEdgeInsets) {
let additionalTopHeight = max(0.0, -self.scrollView.contentOffset.y)
let additionalBottomHeight = -min(0.0, -self.scrollView.contentOffset.y)
self.topDimView.frame = CGRect(x: containerInsets.left + insets.left, y: -additionalTopHeight, width: size.width - containerInsets.left - containerInsets.right - insets.left - insets.right, height: max(0.0, self.itemGroupsContainerNode.frame.minY + additionalTopHeight))
self.bottomDimView.frame = CGRect(x: containerInsets.left + insets.left, y: self.itemGroupsContainerNode.frame.maxY, width: size.width - containerInsets.left - containerInsets.right - insets.left - insets.right, height: max(0.0, size.height - self.itemGroupsContainerNode.frame.maxY + additionalBottomHeight))
self.leftDimView.frame = CGRect(x: 0.0, y: -additionalTopHeight, width: containerInsets.left + insets.left, height: size.height + additionalTopHeight + additionalBottomHeight)
self.rightDimView.frame = CGRect(x: size.width - containerInsets.right - insets.right, y: -additionalTopHeight, width: containerInsets.right + insets.right, height: size.height + additionalTopHeight + additionalBottomHeight)
}
func setGroups(_ groups: [ActionSheetItemGroup]) {
self.itemGroupsContainerNode.setGroups(groups)
}
public func updateItem(groupIndex: Int, itemIndex: Int, _ f: (ActionSheetItem) -> ActionSheetItem) {
self.itemGroupsContainerNode.updateItem(groupIndex: groupIndex, itemIndex: itemIndex, f)
}
}

View File

@ -0,0 +1,6 @@
import Foundation
public protocol ActionSheetItem {
func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode
func updateNode(_ node: ActionSheetItemNode) -> Void
}

View File

@ -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
}
}

View File

@ -0,0 +1,223 @@
import UIKit
import AsyncDisplayKit
private class ActionSheetItemGroupNodeScrollView: UIScrollView {
override func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
}
final class ActionSheetItemGroupNode: ASDisplayNode, UIScrollViewDelegate {
private let theme: ActionSheetControllerTheme
private let centerDimView: UIImageView
private let topDimView: UIView
private let bottomDimView: UIView
let trailingDimView: UIView
private let clippingNode: ASDisplayNode
private let backgroundEffectView: UIVisualEffectView
private let scrollView: UIScrollView
private var itemNodes: [ActionSheetItemNode] = []
private var leadingVisibleNodeCount: CGFloat = 100.0
var respectInputHeight = true
init(theme: ActionSheetControllerTheme) {
self.theme = theme
self.centerDimView = UIImageView()
self.centerDimView.image = generateStretchableFilledCircleImage(radius: 16.0, color: nil, backgroundColor: self.theme.dimColor)
self.topDimView = UIView()
self.topDimView.backgroundColor = self.theme.dimColor
self.topDimView.isUserInteractionEnabled = false
self.bottomDimView = UIView()
self.bottomDimView.backgroundColor = self.theme.dimColor
self.bottomDimView.isUserInteractionEnabled = false
self.trailingDimView = UIView()
self.trailingDimView.backgroundColor = self.theme.dimColor
self.clippingNode = ASDisplayNode()
self.clippingNode.clipsToBounds = true
self.clippingNode.cornerRadius = 16.0
self.backgroundEffectView = UIVisualEffectView(effect: UIBlurEffect(style: self.theme.backgroundType == .light ? .light : .dark))
self.scrollView = ActionSheetItemGroupNodeScrollView()
if #available(iOSApplicationExtension 11.0, *) {
self.scrollView.contentInsetAdjustmentBehavior = .never
}
self.scrollView.delaysContentTouches = false
self.scrollView.canCancelContentTouches = true
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.showsHorizontalScrollIndicator = false
super.init()
self.view.addSubview(self.centerDimView)
self.view.addSubview(self.topDimView)
self.view.addSubview(self.bottomDimView)
self.view.addSubview(self.trailingDimView)
self.scrollView.delegate = self
self.clippingNode.view.addSubview(self.backgroundEffectView)
self.clippingNode.view.addSubview(self.scrollView)
self.addSubnode(self.clippingNode)
}
func updateItemNodes(_ nodes: [ActionSheetItemNode], leadingVisibleNodeCount: CGFloat = 1000.0) {
for node in self.itemNodes {
if !nodes.contains(where: { $0 === node }) {
node.removeFromSupernode()
}
}
for node in nodes {
if !self.itemNodes.contains(where: { $0 === node }) {
self.scrollView.addSubnode(node)
}
}
self.itemNodes = nodes
self.leadingVisibleNodeCount = leadingVisibleNodeCount
self.invalidateCalculatedLayout()
}
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
var itemNodesHeight: CGFloat = 0.0
var leadingVisibleNodeSize: CGFloat = 0.0
var i = 0
for node in self.itemNodes {
if CGFloat(0.0).isLess(than: itemNodesHeight) {
itemNodesHeight += UIScreenPixel
}
let size = node.measure(constrainedSize)
itemNodesHeight += size.height
if ceil(CGFloat(i)).isLessThanOrEqualTo(leadingVisibleNodeCount) {
if CGFloat(0.0).isLess(than: leadingVisibleNodeSize) {
leadingVisibleNodeSize += UIScreenPixel
}
let factor: CGFloat = min(1.0, leadingVisibleNodeCount - CGFloat(i))
leadingVisibleNodeSize += size.height * factor
}
i += 1
}
return CGSize(width: constrainedSize.width, height: min(floorToScreenPixels(itemNodesHeight), constrainedSize.height))
}
override func layout() {
let scrollViewFrame = CGRect(origin: CGPoint(), size: self.calculatedSize)
var updateOffset = false
if !self.scrollView.frame.equalTo(scrollViewFrame) {
self.scrollView.frame = scrollViewFrame
updateOffset = true
}
let backgroundEffectViewFrame = CGRect(origin: CGPoint(), size: self.calculatedSize)
if !self.backgroundEffectView.frame.equalTo(backgroundEffectViewFrame) {
self.backgroundEffectView.frame = backgroundEffectViewFrame
}
var itemNodesHeight: CGFloat = 0.0
var leadingVisibleNodeSize: CGFloat = 0.0
var i = 0
for node in self.itemNodes {
if CGFloat(0.0).isLess(than: itemNodesHeight) {
itemNodesHeight += UIScreenPixel
}
node.frame = CGRect(origin: CGPoint(x: 0.0, y: itemNodesHeight), size: node.calculatedSize)
itemNodesHeight += node.calculatedSize.height
if CGFloat(i).isLessThanOrEqualTo(leadingVisibleNodeCount) {
if CGFloat(0.0).isLess(than: leadingVisibleNodeSize) {
leadingVisibleNodeSize += UIScreenPixel
}
let factor: CGFloat = min(1.0, leadingVisibleNodeCount - CGFloat(i))
leadingVisibleNodeSize += node.calculatedSize.height * factor
}
i += 1
}
let scrollViewContentSize = CGSize(width: self.calculatedSize.width, height: itemNodesHeight)
if !self.scrollView.contentSize.equalTo(scrollViewContentSize) {
self.scrollView.contentSize = scrollViewContentSize
}
let scrollViewContentInsets = UIEdgeInsets(top: max(0.0, self.calculatedSize.height - leadingVisibleNodeSize), left: 0.0, bottom: 0.0, right: 0.0)
if !UIEdgeInsetsEqualToEdgeInsets(self.scrollView.contentInset, scrollViewContentInsets) {
self.scrollView.contentInset = scrollViewContentInsets
}
if updateOffset {
self.scrollView.contentOffset = CGPoint(x: 0.0, y: -scrollViewContentInsets.top)
}
self.updateOverscroll()
}
private func currentVerticalOverscroll() -> CGFloat {
var verticalOverscroll: CGFloat = 0.0
if scrollView.contentOffset.y < 0.0 {
verticalOverscroll = scrollView.contentOffset.y
} else if scrollView.contentOffset.y > scrollView.contentSize.height - scrollView.bounds.size.height {
verticalOverscroll = scrollView.contentOffset.y - (scrollView.contentSize.height - scrollView.bounds.size.height)
}
return verticalOverscroll
}
private func currentRealVerticalOverscroll() -> CGFloat {
var verticalOverscroll: CGFloat = 0.0
if scrollView.contentOffset.y < 0.0 {
verticalOverscroll = scrollView.contentOffset.y
} else if scrollView.contentOffset.y > scrollView.contentSize.height - scrollView.bounds.size.height {
verticalOverscroll = scrollView.contentOffset.y - (scrollView.contentSize.height - scrollView.bounds.size.height)
}
return verticalOverscroll
}
private func updateOverscroll() {
let verticalOverscroll = self.currentVerticalOverscroll()
self.clippingNode.layer.sublayerTransform = CATransform3DMakeTranslation(0.0, min(0.0, verticalOverscroll), 0.0)
let clippingNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: max(0.0, -verticalOverscroll)), size: CGSize(width: self.calculatedSize.width, height: self.calculatedSize.height - abs(verticalOverscroll)))
if !self.clippingNode.frame.equalTo(clippingNodeFrame) {
self.clippingNode.frame = clippingNodeFrame
self.centerDimView.frame = clippingNodeFrame
self.topDimView.frame = CGRect(x: 0.0, y: 0.0, width: clippingNodeFrame.size.width, height: max(0.0, clippingNodeFrame.minY))
self.bottomDimView.frame = CGRect(x: 0.0, y: clippingNodeFrame.maxY, width: clippingNodeFrame.size.width, height: max(0.0, self.bounds.size.height - clippingNodeFrame.maxY))
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.updateOverscroll()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.clippingNode.frame.contains(point) {
return super.hitTest(point, with: event)
} else {
return nil
}
}
func animateDimViewsAlpha(from: CGFloat, to: CGFloat, duration: Double) {
for node in [self.centerDimView, self.topDimView, self.bottomDimView] {
node.layer.animateAlpha(from: from, to: to, duration: duration)
}
}
func itemNode(at index: Int) -> ActionSheetItemNode {
return self.itemNodes[index]
}
}

View File

@ -0,0 +1,99 @@
import UIKit
import AsyncDisplayKit
private let groupSpacing: CGFloat = 8.0
final class ActionSheetItemGroupsContainerNode: ASDisplayNode {
var theme: ActionSheetControllerTheme {
didSet {
self.setGroups(self.groups)
self.setNeedsLayout()
}
}
private var groups: [ActionSheetItemGroup] = []
private var groupNodes: [ActionSheetItemGroupNode] = []
init(theme: ActionSheetControllerTheme) {
self.theme = theme
super.init()
}
func setGroups(_ groups: [ActionSheetItemGroup]) {
self.groups = groups
for groupNode in self.groupNodes {
groupNode.removeFromSupernode()
}
self.groupNodes.removeAll()
for group in groups {
let groupNode = ActionSheetItemGroupNode(theme: self.theme)
groupNode.updateItemNodes(group.items.map({ $0.node(theme: self.theme) }), leadingVisibleNodeCount: group.leadingVisibleNodeCount ?? 1000.0)
self.groupNodes.append(groupNode)
self.addSubnode(groupNode)
}
}
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
var groupsHeight: CGFloat = 0.0
for groupNode in self.groupNodes.reversed() {
if CGFloat(0.0).isLess(than: groupsHeight) {
groupsHeight += groupSpacing
}
let size = groupNode.measure(CGSize(width: constrainedSize.width, height: max(0.0, constrainedSize.height - groupsHeight)))
groupsHeight += size.height
}
return CGSize(width: constrainedSize.width, height: min(groupsHeight, constrainedSize.height))
}
override func layout() {
var groupsHeight: CGFloat = 0.0
for i in 0 ..< self.groupNodes.count {
let groupNode = self.groupNodes[i]
let size = groupNode.calculatedSize
if i != 0 {
groupsHeight += groupSpacing
self.groupNodes[i - 1].trailingDimView.frame = CGRect(x: 0.0, y: groupNodes[i - 1].bounds.size.height, width: size.width, height: groupSpacing)
}
groupNode.frame = CGRect(origin: CGPoint(x: 0.0, y: groupsHeight), size: size)
groupNode.trailingDimView.frame = CGRect()
groupsHeight += size.height
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
for groupNode in self.groupNodes {
if groupNode.frame.contains(point) {
return groupNode.hitTest(self.convert(point, to: groupNode), with: event)
}
}
return nil
}
func animateDimViewsAlpha(from: CGFloat, to: CGFloat, duration: Double) {
for node in self.groupNodes {
node.animateDimViewsAlpha(from: from, to: to, duration: duration)
}
}
public func updateItem(groupIndex: Int, itemIndex: Int, _ f: (ActionSheetItem) -> ActionSheetItem) {
var item = self.groups[groupIndex].items[itemIndex]
let itemNode = self.groupNodes[groupIndex].itemNode(at: itemIndex)
item = f(item)
item.updateNode(itemNode)
var groupItems = self.groups[groupIndex].items
groupItems[itemIndex] = item
self.groups[groupIndex] = ActionSheetItemGroup(items: groupItems)
}
}

View File

@ -0,0 +1,33 @@
import UIKit
import AsyncDisplayKit
open class ActionSheetItemNode: ASDisplayNode {
private let theme: ActionSheetControllerTheme
public let backgroundNode: ASDisplayNode
private let overflowSeparatorNode: ASDisplayNode
public init(theme: ActionSheetControllerTheme) {
self.theme = theme
self.backgroundNode = ASDisplayNode()
self.backgroundNode.backgroundColor = self.theme.itemBackgroundColor
self.overflowSeparatorNode = ASDisplayNode()
self.overflowSeparatorNode.backgroundColor = self.theme.itemHighlightedBackgroundColor
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.overflowSeparatorNode)
}
open override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
return CGSize(width: constrainedSize.width, height: 57.0)
}
open override func layout() {
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: self.calculatedSize)
self.overflowSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: self.calculatedSize.height), size: CGSize(width: self.calculatedSize.width, height: UIScreenPixel))
}
}

View File

@ -0,0 +1,104 @@
import Foundation
import UIKit
import AsyncDisplayKit
public class ActionSheetSwitchItem: ActionSheetItem {
public let title: String
public let isOn: Bool
public let action: (Bool) -> Void
public init(title: String, isOn: Bool, action: @escaping (Bool) -> Void) {
self.title = title
self.isOn = isOn
self.action = action
}
public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
let node = ActionSheetSwitchNode(theme: theme)
node.setItem(self)
return node
}
public func updateNode(_ node: ActionSheetItemNode) {
guard let node = node as? ActionSheetSwitchNode else {
assertionFailure()
return
}
node.setItem(self)
}
}
public class ActionSheetSwitchNode: ActionSheetItemNode {
private let theme: ActionSheetControllerTheme
private var item: ActionSheetSwitchItem?
private let button: HighlightTrackingButton
private let label: ASTextNode
private let switchNode: SwitchNode
override public init(theme: ActionSheetControllerTheme) {
self.theme = theme
self.button = HighlightTrackingButton()
self.label = ASTextNode()
self.label.isUserInteractionEnabled = false
self.label.maximumNumberOfLines = 1
self.label.displaysAsynchronously = false
self.label.truncationMode = .byTruncatingTail
self.switchNode = SwitchNode()
self.switchNode.frameColor = theme.switchFrameColor
self.switchNode.contentColor = theme.switchContentColor
self.switchNode.handleColor = theme.switchHandleColor
super.init(theme: theme)
self.view.addSubview(self.button)
self.label.isUserInteractionEnabled = false
self.addSubnode(self.label)
self.addSubnode(self.switchNode)
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
self.switchNode.valueUpdated = { [weak self] value in
self?.item?.action(value)
}
}
func setItem(_ item: ActionSheetSwitchItem) {
self.item = item
self.label.attributedText = NSAttributedString(string: item.title, font: ActionSheetButtonNode.defaultFont, textColor: self.theme.primaryTextColor)
self.switchNode.isOn = item.isOn
self.setNeedsLayout()
}
public override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
return CGSize(width: constrainedSize.width, height: 57.0)
}
public override func layout() {
super.layout()
let size = self.bounds.size
self.button.frame = CGRect(origin: CGPoint(), size: size)
let labelSize = self.label.measure(CGSize(width: max(1.0, size.width - 51.0 - 16.0 * 2.0), height: size.height))
self.label.frame = CGRect(origin: CGPoint(x: 16.0, y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize)
let switchSize = CGSize(width: 51.0, height: 31.0)
self.switchNode.frame = CGRect(origin: CGPoint(x: size.width - 16.0 - switchSize.width, y: floor((size.height - switchSize.height) / 2.0)), size: switchSize)
}
@objc func buttonPressed() {
let value = !self.switchNode.isOn
self.switchNode.setOn(value, animated: true)
self.item?.action(value)
}
}

View File

@ -0,0 +1,73 @@
import Foundation
import UIKit
import AsyncDisplayKit
public class ActionSheetTextItem: ActionSheetItem {
public let title: String
public init(title: String) {
self.title = title
}
public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
let node = ActionSheetTextNode(theme: theme)
node.setItem(self)
return node
}
public func updateNode(_ node: ActionSheetItemNode) {
guard let node = node as? ActionSheetTextNode else {
assertionFailure()
return
}
node.setItem(self)
}
}
public class ActionSheetTextNode: ActionSheetItemNode {
public static let defaultFont: UIFont = Font.regular(13.0)
private let theme: ActionSheetControllerTheme
private var item: ActionSheetTextItem?
private let label: ASTextNode
override public init(theme: ActionSheetControllerTheme) {
self.theme = theme
self.label = ASTextNode()
self.label.isUserInteractionEnabled = false
self.label.maximumNumberOfLines = 0
self.label.displaysAsynchronously = false
self.label.truncationMode = .byTruncatingTail
super.init(theme: theme)
self.label.isUserInteractionEnabled = false
self.addSubnode(self.label)
}
func setItem(_ item: ActionSheetTextItem) {
self.item = item
self.label.attributedText = NSAttributedString(string: item.title, font: ActionSheetTextNode.defaultFont, textColor: self.theme.secondaryTextColor, paragraphAlignment: .center)
self.setNeedsLayout()
}
public override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
let labelSize = self.label.measure(CGSize(width: max(1.0, constrainedSize.width - 20.0), height: constrainedSize.height))
return CGSize(width: constrainedSize.width, height: max(57.0, labelSize.height + 32.0))
}
public override func layout() {
super.layout()
let size = self.bounds.size
let labelSize = self.label.measure(CGSize(width: max(1.0, size.width - 20.0), height: size.height))
self.label.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - labelSize.width) / 2.0), y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize)
}
}

View File

@ -0,0 +1,87 @@
import Foundation
import UIKit
public enum ActionSheetControllerThemeBackgroundType {
case light
case dark
}
public final class ActionSheetControllerTheme: Equatable {
public let dimColor: UIColor
public let backgroundType: ActionSheetControllerThemeBackgroundType
public let itemBackgroundColor: UIColor
public let itemHighlightedBackgroundColor: UIColor
public let standardActionTextColor: UIColor
public let destructiveActionTextColor: UIColor
public let disabledActionTextColor: UIColor
public let primaryTextColor: UIColor
public let secondaryTextColor: UIColor
public let controlAccentColor: UIColor
public let controlColor: UIColor
public let switchFrameColor: UIColor
public let switchContentColor: UIColor
public let switchHandleColor: UIColor
public init(dimColor: UIColor, backgroundType: ActionSheetControllerThemeBackgroundType, itemBackgroundColor: UIColor, itemHighlightedBackgroundColor: UIColor, standardActionTextColor: UIColor, destructiveActionTextColor: UIColor, disabledActionTextColor: UIColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, controlAccentColor: UIColor, controlColor: UIColor, switchFrameColor: UIColor, switchContentColor: UIColor, switchHandleColor: UIColor) {
self.dimColor = dimColor
self.backgroundType = backgroundType
self.itemBackgroundColor = itemBackgroundColor
self.itemHighlightedBackgroundColor = itemHighlightedBackgroundColor
self.standardActionTextColor = standardActionTextColor
self.destructiveActionTextColor = destructiveActionTextColor
self.disabledActionTextColor = disabledActionTextColor
self.primaryTextColor = primaryTextColor
self.secondaryTextColor = secondaryTextColor
self.controlAccentColor = controlAccentColor
self.controlColor = controlColor
self.switchFrameColor = switchFrameColor
self.switchContentColor = switchContentColor
self.switchHandleColor = switchHandleColor
}
public static func ==(lhs: ActionSheetControllerTheme, rhs: ActionSheetControllerTheme) -> Bool {
if lhs.dimColor != rhs.dimColor {
return false
}
if lhs.backgroundType != rhs.backgroundType {
return false
}
if lhs.itemBackgroundColor != rhs.itemBackgroundColor {
return false
}
if lhs.itemHighlightedBackgroundColor != rhs.itemHighlightedBackgroundColor {
return false
}
if lhs.standardActionTextColor != rhs.standardActionTextColor {
return false
}
if lhs.destructiveActionTextColor != rhs.destructiveActionTextColor {
return false
}
if lhs.disabledActionTextColor != rhs.disabledActionTextColor {
return false
}
if lhs.primaryTextColor != rhs.primaryTextColor {
return false
}
if lhs.secondaryTextColor != rhs.secondaryTextColor {
return false
}
if lhs.controlAccentColor != rhs.controlAccentColor {
return false
}
if lhs.controlColor != rhs.controlColor {
return false
}
if lhs.switchFrameColor != rhs.switchFrameColor {
return false
}
if lhs.switchContentColor != rhs.switchContentColor {
return false
}
if lhs.switchHandleColor != rhs.switchHandleColor {
return false
}
return true
}
}

View File

@ -0,0 +1,21 @@
import Foundation
import UIKit
import AsyncDisplayKit
open class AlertContentNode: ASDisplayNode {
open var requestLayout: ((ContainedViewLayoutTransition) -> Void)?
open var dismissOnOutsideTap: Bool {
return true
}
open func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
assertionFailure()
return CGSize()
}
open func updateTheme(_ theme: AlertControllerTheme) {
}
}

View File

@ -0,0 +1,133 @@
import Foundation
import UIKit
import AsyncDisplayKit
public enum AlertControllerThemeBackgroundType {
case light
case dark
}
public final class AlertControllerTheme: Equatable {
public let backgroundType: ActionSheetControllerThemeBackgroundType
public let backgroundColor: UIColor
public let separatorColor: UIColor
public let highlightedItemColor: UIColor
public let primaryColor: UIColor
public let secondaryColor: UIColor
public let accentColor: UIColor
public let destructiveColor: UIColor
public let disabledColor: UIColor
public init(backgroundType: ActionSheetControllerThemeBackgroundType, backgroundColor: UIColor, separatorColor: UIColor, highlightedItemColor: UIColor, primaryColor: UIColor, secondaryColor: UIColor, accentColor: UIColor, destructiveColor: UIColor, disabledColor: UIColor) {
self.backgroundType = backgroundType
self.backgroundColor = backgroundColor
self.separatorColor = separatorColor
self.highlightedItemColor = highlightedItemColor
self.primaryColor = primaryColor
self.secondaryColor = secondaryColor
self.accentColor = accentColor
self.destructiveColor = destructiveColor
self.disabledColor = disabledColor
}
public static func ==(lhs: AlertControllerTheme, rhs: AlertControllerTheme) -> Bool {
if lhs.backgroundType != rhs.backgroundType {
return false
}
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
if lhs.separatorColor != rhs.separatorColor {
return false
}
if lhs.highlightedItemColor != rhs.highlightedItemColor {
return false
}
if lhs.primaryColor != rhs.primaryColor {
return false
}
if lhs.secondaryColor != rhs.secondaryColor {
return false
}
if lhs.accentColor != rhs.accentColor {
return false
}
if lhs.destructiveColor != rhs.destructiveColor {
return false
}
if lhs.disabledColor != rhs.disabledColor {
return false
}
return true
}
}
open class AlertController: ViewController {
private var controllerNode: AlertControllerNode {
return self.displayNode as! AlertControllerNode
}
public var theme: AlertControllerTheme {
didSet {
if oldValue != self.theme {
self.controllerNode.updateTheme(self.theme)
}
}
}
private let contentNode: AlertContentNode
private let allowInputInset: Bool
public var dismissed: (() -> Void)?
public init(theme: AlertControllerTheme, contentNode: AlertContentNode, allowInputInset: Bool = true) {
self.theme = theme
self.contentNode = contentNode
self.allowInputInset = allowInputInset
super.init(navigationBarPresentationData: nil)
self.blocksBackgroundWhenInOverlay = true
self.statusBar.statusBarStyle = .Ignore
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override open func loadDisplayNode() {
self.displayNode = AlertControllerNode(contentNode: self.contentNode, theme: self.theme, allowInputInset: self.allowInputInset)
self.displayNodeDidLoad()
self.controllerNode.dismiss = { [weak self] in
if let strongSelf = self, strongSelf.contentNode.dismissOnOutsideTap {
strongSelf.controllerNode.animateOut {
self?.dismiss()
}
}
}
}
override open func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.controllerNode.animateIn()
}
override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, transition: transition)
}
override open func dismiss(completion: (() -> Void)? = nil) {
self.dismissed?()
self.presentingViewController?.dismiss(animated: false, completion: completion)
}
public func dismissAnimated() {
self.controllerNode.animateOut { [weak self] in
self?.dismiss()
}
}
}

View File

@ -0,0 +1,147 @@
import Foundation
import UIKit
import AsyncDisplayKit
final class AlertControllerNode: ASDisplayNode {
private let centerDimView: UIImageView
private let topDimView: UIView
private let bottomDimView: UIView
private let leftDimView: UIView
private let rightDimView: UIView
private let containerNode: ASDisplayNode
private let effectNode: ASDisplayNode
private let backgroundNode: ASDisplayNode
private let contentNode: AlertContentNode
private let allowInputInset: Bool
private var containerLayout: ContainerViewLayout?
var dismiss: (() -> Void)?
init(contentNode: AlertContentNode, theme: AlertControllerTheme, allowInputInset: Bool) {
self.allowInputInset = allowInputInset
let dimColor = UIColor(white: 0.0, alpha: 0.5)
self.centerDimView = UIImageView()
self.centerDimView.image = generateStretchableFilledCircleImage(radius: 16.0, color: nil, backgroundColor: dimColor)
self.topDimView = UIView()
self.topDimView.backgroundColor = dimColor
self.bottomDimView = UIView()
self.bottomDimView.backgroundColor = dimColor
self.leftDimView = UIView()
self.leftDimView.backgroundColor = dimColor
self.rightDimView = UIView()
self.rightDimView.backgroundColor = dimColor
self.containerNode = ASDisplayNode()
self.containerNode.layer.cornerRadius = 14.0
self.containerNode.layer.masksToBounds = true
self.backgroundNode = ASDisplayNode()
self.backgroundNode.backgroundColor = theme.backgroundColor
self.effectNode = ASDisplayNode(viewBlock: {
return UIVisualEffectView(effect: UIBlurEffect(style: theme.backgroundType == .light ? .light : .dark))
})
self.contentNode = contentNode
super.init()
self.view.addSubview(self.centerDimView)
self.view.addSubview(self.topDimView)
self.view.addSubview(self.bottomDimView)
self.view.addSubview(self.leftDimView)
self.view.addSubview(self.rightDimView)
self.containerNode.addSubnode(self.effectNode)
self.containerNode.addSubnode(self.backgroundNode)
self.containerNode.addSubnode(self.contentNode)
self.addSubnode(self.containerNode)
self.contentNode.requestLayout = { [weak self] transition in
if let strongSelf = self, let containerLayout = self?.containerLayout {
strongSelf.containerLayoutUpdated(containerLayout, transition: transition)
}
}
}
override func didLoad() {
super.didLoad()
self.topDimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimmingNodeTapGesture(_:))))
self.bottomDimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimmingNodeTapGesture(_:))))
self.leftDimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimmingNodeTapGesture(_:))))
self.rightDimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimmingNodeTapGesture(_:))))
}
func updateTheme(_ theme: AlertControllerTheme) {
if let effectView = self.effectNode.view as? UIVisualEffectView {
effectView.effect = UIBlurEffect(style: theme.backgroundType == .light ? .light : .dark)
}
self.backgroundNode.backgroundColor = theme.backgroundColor
self.contentNode.updateTheme(theme)
}
func animateIn() {
self.centerDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.topDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.bottomDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.leftDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.rightDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
self.containerNode.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, removeOnCompletion: true, additive: false, completion: nil)
}
func animateOut(completion: @escaping () -> Void) {
self.centerDimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
self.topDimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
self.bottomDimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
self.leftDimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
self.rightDimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
self.containerNode.layer.animateScale(from: 1.0, to: 0.8, duration: 0.4, removeOnCompletion: false, completion: { _ in
completion()
})
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
self.containerLayout = layout
var insetOptions: ContainerViewLayoutInsetOptions = [.statusBar]
if self.allowInputInset {
insetOptions.insert(.input)
}
var insets = layout.insets(options: insetOptions)
let maxWidth = min(240.0, layout.size.width - 70.0)
insets.left = floor((layout.size.width - maxWidth) / 2.0)
insets.right = floor((layout.size.width - maxWidth) / 2.0)
let contentAvailableFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: layout.size.width - insets.right, height: layout.size.height - insets.top - insets.bottom))
let contentSize = self.contentNode.updateLayout(size: contentAvailableFrame.size, transition: transition)
let containerSize = CGSize(width: contentSize.width, height: contentSize.height)
let containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: contentAvailableFrame.minY + floor((contentAvailableFrame.size.height - containerSize.height) / 2.0)), size: containerSize)
transition.updateFrame(view: self.centerDimView, frame: containerFrame)
transition.updateFrame(view: self.topDimView, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: containerFrame.minY)))
transition.updateFrame(view: self.bottomDimView, frame: CGRect(origin: CGPoint(x: 0.0, y: containerFrame.maxY), size: CGSize(width: layout.size.width, height: layout.size.height - containerFrame.maxY)))
transition.updateFrame(view: self.leftDimView, frame: CGRect(origin: CGPoint(x: 0.0, y: containerFrame.minY), size: CGSize(width: containerFrame.minX, height: containerFrame.height)))
transition.updateFrame(view: self.rightDimView, frame: CGRect(origin: CGPoint(x: containerFrame.maxX, y: containerFrame.minY), size: CGSize(width: layout.size.width - containerFrame.maxX, height: containerFrame.height)))
transition.updateFrame(node: self.containerNode, frame: containerFrame)
transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(), size: containerFrame.size))
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: containerFrame.size))
transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: containerFrame.size))
}
@objc func dimmingNodeTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.dismiss?()
}
}
}

View File

@ -0,0 +1,326 @@
import UIKit
#if BUCK
import DisplayPrivate
#endif
@objc private class CALayerAnimationDelegate: NSObject, CAAnimationDelegate {
private let keyPath: String?
var completion: ((Bool) -> Void)?
init(animation: CAAnimation, completion: ((Bool) -> Void)?) {
if let animation = animation as? CABasicAnimation {
self.keyPath = animation.keyPath
} else {
self.keyPath = nil
}
self.completion = completion
super.init()
}
@objc func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
if let anim = anim as? CABasicAnimation {
if anim.keyPath != self.keyPath {
return
}
}
if let completion = self.completion {
completion(flag)
}
}
}
private let completionKey = "CAAnimationUtils_completion"
public let kCAMediaTimingFunctionSpring = "CAAnimationUtilsSpringCurve"
public extension CAAnimation {
public var completion: ((Bool) -> Void)? {
get {
if let delegate = self.delegate as? CALayerAnimationDelegate {
return delegate.completion
} else {
return nil
}
} set(value) {
if let delegate = self.delegate as? CALayerAnimationDelegate {
delegate.completion = value
} else {
self.delegate = CALayerAnimationDelegate(animation: self, completion: value)
}
}
}
}
public extension CALayer {
public func makeAnimation(from: AnyObject, to: AnyObject, keyPath: String, timingFunction: String, duration: Double, delay: Double = 0.0, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) -> CAAnimation {
if timingFunction == kCAMediaTimingFunctionSpring {
let animation = makeSpringAnimation(keyPath)
animation.fromValue = from
animation.toValue = to
animation.isRemovedOnCompletion = removeOnCompletion
animation.fillMode = kCAFillModeForwards
if let completion = completion {
animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion)
}
let k = Float(UIView.animationDurationFactor())
var speed: Float = 1.0
if k != 0 && k != 1 {
speed = Float(1.0) / k
}
animation.speed = speed * Float(animation.duration / duration)
animation.isAdditive = additive
if !delay.isZero {
animation.beginTime = CACurrentMediaTime() + delay
animation.fillMode = kCAFillModeBoth
}
return animation
} else {
let k = Float(UIView.animationDurationFactor())
var speed: Float = 1.0
if k != 0 && k != 1 {
speed = Float(1.0) / k
}
let animation = CABasicAnimation(keyPath: keyPath)
animation.fromValue = from
animation.toValue = to
animation.duration = duration
if let mediaTimingFunction = mediaTimingFunction {
animation.timingFunction = mediaTimingFunction
} else {
animation.timingFunction = CAMediaTimingFunction(name: timingFunction)
}
animation.isRemovedOnCompletion = removeOnCompletion
animation.fillMode = kCAFillModeForwards
animation.speed = speed
animation.isAdditive = additive
if let completion = completion {
animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion)
}
if !delay.isZero {
animation.beginTime = CACurrentMediaTime() + delay
animation.fillMode = kCAFillModeBoth
}
return animation
}
}
public func animate(from: AnyObject, to: AnyObject, keyPath: String, timingFunction: String, duration: Double, delay: Double = 0.0, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
let animation = self.makeAnimation(from: from, to: to, keyPath: keyPath, timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion)
self.add(animation, forKey: additive ? nil : keyPath)
}
public func animateGroup(_ animations: [CAAnimation], key: String) {
let animationGroup = CAAnimationGroup()
var timeOffset = 0.0
for animation in animations {
animation.beginTime = animation.beginTime + timeOffset
timeOffset += animation.duration / Double(animation.speed)
}
animationGroup.animations = animations
animationGroup.duration = timeOffset
self.add(animationGroup, forKey: key)
}
public func animateKeyframes(values: [AnyObject], duration: Double, keyPath: String, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
let k = Float(UIView.animationDurationFactor())
var speed: Float = 1.0
if k != 0 && k != 1 {
speed = Float(1.0) / k
}
let animation = CAKeyframeAnimation(keyPath: keyPath)
animation.values = values
var keyTimes: [NSNumber] = []
for i in 0 ..< values.count {
if i == 0 {
keyTimes.append(0.0)
} else if i == values.count - 1 {
keyTimes.append(1.0)
} else {
keyTimes.append((Double(i) / Double(values.count - 1)) as NSNumber)
}
}
animation.keyTimes = keyTimes
animation.speed = speed
animation.duration = duration
if let completion = completion {
animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion)
}
self.add(animation, forKey: keyPath)
}
public func animateSpring(from: AnyObject, to: AnyObject, keyPath: String, duration: Double, initialVelocity: CGFloat = 0.0, damping: CGFloat = 88.0, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
let animation: CABasicAnimation
if #available(iOS 9.0, *) {
animation = makeSpringBounceAnimation(keyPath, initialVelocity, damping)
} else {
animation = makeSpringAnimation(keyPath)
}
animation.fromValue = from
animation.toValue = to
animation.isRemovedOnCompletion = removeOnCompletion
animation.fillMode = kCAFillModeForwards
if let completion = completion {
animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion)
}
let k = Float(UIView.animationDurationFactor())
var speed: Float = 1.0
if k != 0 && k != 1 {
speed = Float(1.0) / k
}
animation.speed = speed * Float(animation.duration / duration)
animation.isAdditive = additive
self.add(animation, forKey: keyPath)
}
public func animateAdditive(from: NSValue, to: NSValue, keyPath: String, key: String, timingFunction: String, mediaTimingFunction: CAMediaTimingFunction? = nil, duration: Double, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
let k = Float(UIView.animationDurationFactor())
var speed: Float = 1.0
if k != 0 && k != 1 {
speed = Float(1.0) / k
}
let animation = CABasicAnimation(keyPath: keyPath)
animation.fromValue = from
animation.toValue = to
animation.duration = duration
if let mediaTimingFunction = mediaTimingFunction {
animation.timingFunction = mediaTimingFunction
} else {
animation.timingFunction = CAMediaTimingFunction(name: timingFunction)
}
animation.isRemovedOnCompletion = removeOnCompletion
animation.fillMode = kCAFillModeForwards
animation.speed = speed
animation.isAdditive = true
if let completion = completion {
animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion)
}
self.add(animation, forKey: key)
}
public func animateAlpha(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = kCAMediaTimingFunctionEaseInEaseOut, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> ())? = nil) {
self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "opacity", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: completion)
}
public func animateScale(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = kCAMediaTimingFunctionEaseInEaseOut, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "transform.scale", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: completion)
}
public func animateRotation(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = kCAMediaTimingFunctionEaseInEaseOut, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "transform.rotation.z", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: completion)
}
func animatePosition(from: CGPoint, to: CGPoint, duration: Double, delay: Double = 0.0, timingFunction: String = kCAMediaTimingFunctionEaseInEaseOut, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, force: Bool = false, completion: ((Bool) -> Void)? = nil) {
if from == to && !force {
if let completion = completion {
completion(true)
}
return
}
self.animate(from: NSValue(cgPoint: from), to: NSValue(cgPoint: to), keyPath: "position", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion)
}
func animateBounds(from: CGRect, to: CGRect, duration: Double, timingFunction: String, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, force: Bool = false, completion: ((Bool) -> Void)? = nil) {
if from == to && !force {
if let completion = completion {
completion(true)
}
return
}
self.animate(from: NSValue(cgRect: from), to: NSValue(cgRect: to), keyPath: "bounds", timingFunction: timingFunction, duration: duration, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion)
}
public func animateBoundsOriginXAdditive(from: CGFloat, to: CGFloat, duration: Double, timingFunction: String = kCAMediaTimingFunctionEaseInEaseOut, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
self.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.origin.x", timingFunction: timingFunction, duration: duration, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion)
}
public func animateBoundsOriginYAdditive(from: CGFloat, to: CGFloat, duration: Double, timingFunction: String = kCAMediaTimingFunctionEaseInEaseOut, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
self.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.origin.y", timingFunction: timingFunction, duration: duration, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion)
}
public func animateBoundsOriginXAdditive(from: CGFloat, to: CGFloat, duration: Double, mediaTimingFunction: CAMediaTimingFunction) {
self.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.origin.x", timingFunction: kCAMediaTimingFunctionEaseInEaseOut, duration: duration, mediaTimingFunction: mediaTimingFunction, additive: true)
}
public func animateBoundsOriginYAdditive(from: CGFloat, to: CGFloat, duration: Double, mediaTimingFunction: CAMediaTimingFunction) {
self.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.origin.y", timingFunction: kCAMediaTimingFunctionEaseInEaseOut, duration: duration, mediaTimingFunction: mediaTimingFunction, additive: true)
}
public func animatePositionKeyframes(values: [CGPoint], duration: Double, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
self.animateKeyframes(values: values.map { NSValue(cgPoint: $0) }, duration: duration, keyPath: "position")
}
public func animateFrame(from: CGRect, to: CGRect, duration: Double, timingFunction: String, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, force: Bool = false, completion: ((Bool) -> Void)? = nil) {
if from == to && !force {
if let completion = completion {
completion(true)
}
return
}
var interrupted = false
var completedPosition = false
var completedBounds = false
let partialCompletion: () -> Void = {
if interrupted || (completedPosition && completedBounds) {
if let completion = completion {
completion(!interrupted)
}
}
}
var fromPosition = CGPoint(x: from.midX, y: from.midY)
var toPosition = CGPoint(x: to.midX, y: to.midY)
var fromBounds = CGRect(origin: self.bounds.origin, size: from.size)
var toBounds = CGRect(origin: self.bounds.origin, size: to.size)
if additive {
fromPosition.x = -(toPosition.x - fromPosition.x)
fromPosition.y = -(toPosition.y - fromPosition.y)
toPosition = CGPoint()
fromBounds.size.width = -(toBounds.width - fromBounds.width)
fromBounds.size.height = -(toBounds.height - fromBounds.height)
toBounds = CGRect()
}
self.animatePosition(from: fromPosition, to: toPosition, duration: duration, timingFunction: timingFunction, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, force: force, completion: { value in
if !value {
interrupted = true
}
completedPosition = true
partialCompletion()
})
self.animateBounds(from: fromBounds, to: toBounds, duration: duration, timingFunction: timingFunction, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, force: force, completion: { value in
if !value {
interrupted = true
}
completedBounds = true
partialCompletion()
})
}
public func cancelAnimationsRecursive(key: String) {
self.removeAnimation(forKey: key)
if let sublayers = self.sublayers {
for layer in sublayers {
layer.cancelAnimationsRecursive(key: key)
}
}
}
}

View File

@ -0,0 +1,9 @@
#import <UIKit/UIKit.h>
@interface CASeeThroughTracingLayer : CALayer
@end
@interface CASeeThroughTracingView : UIView
@end

View File

@ -0,0 +1,57 @@
#import "CASeeThroughTracingLayer.h"
@interface CASeeThroughTracingLayer () {
CGPoint _parentOffset;
}
@end
@implementation CASeeThroughTracingLayer
- (void)addAnimation:(CAAnimation *)anim forKey:(NSString *)key {
[super addAnimation:anim forKey:key];
}
- (void)setFrame:(CGRect)frame {
[super setFrame:frame];
[self _mirrorTransformToSublayers];
}
- (void)setBounds:(CGRect)bounds {
[super setBounds:bounds];
[self _mirrorTransformToSublayers];
}
- (void)setPosition:(CGPoint)position {
[super setPosition:position];
[self _mirrorTransformToSublayers];
}
- (void)_mirrorTransformToSublayers {
CGRect bounds = self.bounds;
CGPoint position = self.position;
CGPoint sublayerParentOffset = _parentOffset;
sublayerParentOffset.x += position.x - (bounds.size.width) / 2.0f;
sublayerParentOffset.y += position.y - (bounds.size.width) / 2.0f;
for (CALayer *sublayer in self.sublayers) {
if ([sublayer isKindOfClass:[CASeeThroughTracingLayer class]]) {
((CASeeThroughTracingLayer *)sublayer)->_parentOffset = sublayerParentOffset;
[(CASeeThroughTracingLayer *)sublayer _mirrorTransformToSublayers];
}
}
}
@end
@implementation CASeeThroughTracingView
+ (Class)layerClass {
return [CASeeThroughTracingLayer class];
}
@end

View File

@ -0,0 +1,34 @@
#import <UIKit/UIKit.h>
@interface CATracingLayer : CALayer
@end
@interface CATracingLayerInfo : NSObject
@property (nonatomic, readonly) bool shouldBeAdjustedToInverseTransform;
@property (nonatomic, weak, readonly) id _Nullable userData;
@property (nonatomic, readonly) int32_t tracingTag;
@property (nonatomic, readonly) int32_t disableChildrenTracingTags;
- (instancetype _Nonnull)initWithShouldBeAdjustedToInverseTransform:(bool)shouldBeAdjustedToInverseTransform userData:(id _Nullable)userData tracingTag:(int32_t)tracingTag disableChildrenTracingTags:(int32_t)disableChildrenTracingTags;
@end
@interface CALayer (Tracing)
- (CATracingLayerInfo * _Nullable)traceableInfo;
- (void)setTraceableInfo:(CATracingLayerInfo * _Nullable)info;
- (bool)hasPositionOrOpacityAnimations;
- (bool)hasPositionAnimations;
- (void)setInvalidateTracingSublayers:(void (^_Nullable)())block;
- (NSArray<NSArray<CALayer *> *> * _Nonnull)traceableLayerSurfacesWithTag:(int32_t)tracingTag;
- (void)adjustTraceableLayerTransforms:(CGSize)offset;
- (void)setPositionAnimationMirrorTarget:(CALayer * _Nullable)animationMirrorTarget;
- (void)invalidateUpTheTree;
@end

View File

@ -0,0 +1,364 @@
#import "CATracingLayer.h"
#import "RuntimeUtils.h"
static void *CATracingLayerInvalidatedKey = &CATracingLayerInvalidatedKey;
static void *CATracingLayerIsInvalidatedBlock = &CATracingLayerIsInvalidatedBlock;
static void *CATracingLayerTraceableInfoKey = &CATracingLayerTraceableInfoKey;
static void *CATracingLayerPositionAnimationMirrorTarget = &CATracingLayerPositionAnimationMirrorTarget;
@implementation CALayer (Tracing)
- (void)setInvalidateTracingSublayers:(void (^_Nullable)())block {
[self setAssociatedObject:[block copy] forKey:CATracingLayerIsInvalidatedBlock];
}
- (void (^_Nullable)())invalidateTracingSublayers {
return [self associatedObjectForKey:CATracingLayerIsInvalidatedBlock];
}
- (bool)isTraceable {
return [self associatedObjectForKey:CATracingLayerTraceableInfoKey] != nil || [self isKindOfClass:[CATracingLayer class]];
}
- (CATracingLayerInfo * _Nullable)traceableInfo {
return [self associatedObjectForKey:CATracingLayerTraceableInfoKey];
}
- (void)setTraceableInfo:(CATracingLayerInfo * _Nullable)info {
[self setAssociatedObject:info forKey:CATracingLayerTraceableInfoKey];
}
- (bool)hasPositionOrOpacityAnimations {
return [self animationForKey:@"position"] != nil || [self animationForKey:@"bounds"] != nil || [self animationForKey:@"sublayerTransform"] != nil || [self animationForKey:@"opacity"] != nil;
}
- (bool)hasPositionAnimations {
return [self animationForKey:@"position"] != nil || [self animationForKey:@"bounds"] != nil;
}
static void traceLayerSurfaces(int32_t tracingTag, int depth, CALayer * _Nonnull layer, NSMutableDictionary<NSNumber *, NSMutableArray<CALayer *> *> *layersByDepth, bool skipIfNoTraceableSublayers) {
bool hadTraceableSublayers = false;
for (CALayer *sublayer in layer.sublayers.reverseObjectEnumerator) {
CATracingLayerInfo *sublayerTraceableInfo = [sublayer traceableInfo];
if (sublayerTraceableInfo != nil && sublayerTraceableInfo.tracingTag == tracingTag) {
NSMutableArray *array = layersByDepth[@(depth)];
if (array == nil) {
array = [[NSMutableArray alloc] init];
layersByDepth[@(depth)] = array;
}
[array addObject:sublayer];
hadTraceableSublayers = true;
}
if (sublayerTraceableInfo.disableChildrenTracingTags & tracingTag) {
return;
}
}
if (!skipIfNoTraceableSublayers || !hadTraceableSublayers) {
for (CALayer *sublayer in layer.sublayers.reverseObjectEnumerator) {
if ([sublayer isKindOfClass:[CATracingLayer class]]) {
traceLayerSurfaces(tracingTag, depth + 1, sublayer, layersByDepth, hadTraceableSublayers);
}
}
}
}
- (NSArray<NSArray<CALayer *> *> * _Nonnull)traceableLayerSurfacesWithTag:(int32_t)tracingTag {
NSMutableDictionary<NSNumber *, NSMutableArray<CALayer *> *> *layersByDepth = [[NSMutableDictionary alloc] init];
traceLayerSurfaces(tracingTag, 0, self, layersByDepth, false);
NSMutableArray<NSMutableArray<CALayer *> *> *result = [[NSMutableArray alloc] init];
for (id key in [[layersByDepth allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
[result addObject:layersByDepth[key]];
}
return result;
}
- (void)adjustTraceableLayerTransforms:(CGSize)offset {
CGRect frame = self.frame;
CGSize sublayerOffset = CGSizeMake(frame.origin.x + offset.width, frame.origin.y + offset.height);
for (CALayer *sublayer in self.sublayers) {
CATracingLayerInfo *sublayerTraceableInfo = [sublayer traceableInfo];
if (sublayerTraceableInfo != nil && sublayerTraceableInfo.shouldBeAdjustedToInverseTransform) {
sublayer.sublayerTransform = CATransform3DMakeTranslation(-sublayerOffset.width, -sublayerOffset.height, 0.0f);
} else if ([sublayer isKindOfClass:[CATracingLayer class]]) {
[(CATracingLayer *)sublayer adjustTraceableLayerTransforms:sublayerOffset];
}
}
}
- (CALayer * _Nullable)animationMirrorTarget {
return [self associatedObjectForKey:CATracingLayerPositionAnimationMirrorTarget];
}
- (void)setPositionAnimationMirrorTarget:(CALayer * _Nullable)animationMirrorTarget {
[self setAssociatedObject:animationMirrorTarget forKey:CATracingLayerPositionAnimationMirrorTarget associationPolicy:NSObjectAssociationPolicyRetain];
}
- (void)invalidateUpTheTree {
CALayer *superlayer = self;
while (true) {
if (superlayer == nil) {
break;
}
void (^block)() = [superlayer invalidateTracingSublayers];
if (block != nil) {
block();
}
superlayer = superlayer.superlayer;
}
}
@end
@interface CATracingLayerAnimationDelegate : NSObject <CAAnimationDelegate> {
id<CAAnimationDelegate> _delegate;
void (^_animationStopped)();
}
@end
@implementation CATracingLayerAnimationDelegate
- (instancetype)initWithDelegate:(id<CAAnimationDelegate>)delegate animationStopped:(void (^_Nonnull)())animationStopped {
_delegate = delegate;
_animationStopped = [animationStopped copy];
return self;
}
- (void)animationDidStart:(CAAnimation *)anim {
if ([_delegate respondsToSelector:@selector(animationDidStart:)]) {
[(id)_delegate animationDidStart:anim];
}
}
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
if ([_delegate respondsToSelector:@selector(animationDidStop:finished:)]) {
[(id)_delegate animationDidStop:anim finished:flag];
}
if (_animationStopped) {
_animationStopped();
}
}
@end
@interface CATracingLayer ()
@property (nonatomic) bool isInvalidated;
@end
@implementation CATracingLayer
- (void)setNeedsDisplay {
}
- (void)displayIfNeeded {
}
- (bool)isInvalidated {
return [[self associatedObjectForKey:CATracingLayerInvalidatedKey] intValue] != 0;
}
- (void)setIsInvalidated:(bool)isInvalidated {
[self setAssociatedObject: isInvalidated ? @1 : @0 forKey:CATracingLayerInvalidatedKey];
}
- (void)setPosition:(CGPoint)position {
[super setPosition:position];
[self invalidateUpTheTree];
}
- (void)setOpacity:(float)opacity {
[super setOpacity:opacity];
[self invalidateUpTheTree];
}
- (void)addSublayer:(CALayer *)layer {
[super addSublayer:layer];
if ([layer isTraceable] || [layer isKindOfClass:[CATracingLayer class]]) {
[self invalidateUpTheTree];
}
}
- (void)insertSublayer:(CALayer *)layer atIndex:(unsigned)idx {
[super insertSublayer:layer atIndex:idx];
if ([layer isTraceable] || [layer isKindOfClass:[CATracingLayer class]]) {
[self invalidateUpTheTree];
}
}
- (void)insertSublayer:(CALayer *)layer below:(nullable CALayer *)sibling {
[super insertSublayer:layer below:sibling];
if ([layer isTraceable] || [layer isKindOfClass:[CATracingLayer class]]) {
[self invalidateUpTheTree];
}
}
- (void)insertSublayer:(CALayer *)layer above:(nullable CALayer *)sibling {
[super insertSublayer:layer above:sibling];
if ([layer isTraceable] || [layer isKindOfClass:[CATracingLayer class]]) {
[self invalidateUpTheTree];
}
}
- (void)replaceSublayer:(CALayer *)layer with:(CALayer *)layer2 {
[super replaceSublayer:layer with:layer2];
if ([layer isTraceable] || [layer2 isTraceable]) {
[self invalidateUpTheTree];
}
}
- (void)removeFromSuperlayer {
if ([self isTraceable]) {
[self invalidateUpTheTree];
}
[super removeFromSuperlayer];
}
- (void)addAnimation:(CAAnimation *)anim forKey:(NSString *)key {
if ([anim isKindOfClass:[CABasicAnimation class]]) {
if (false && [key isEqualToString:@"bounds.origin.y"]) {
CABasicAnimation *animCopy = [anim copy];
CGFloat from = [animCopy.fromValue floatValue];
CGFloat to = [animCopy.toValue floatValue];
animCopy.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(0.0, to - from, 0.0f)];
animCopy.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
animCopy.keyPath = @"sublayerTransform";
__weak CATracingLayer *weakSelf = self;
anim.delegate = [[CATracingLayerAnimationDelegate alloc] initWithDelegate:anim.delegate animationStopped:^{
__strong CATracingLayer *strongSelf = weakSelf;
if (strongSelf != nil) {
[strongSelf invalidateUpTheTree];
}
}];
[super addAnimation:anim forKey:key];
CABasicAnimation *positionAnimCopy = [animCopy copy];
positionAnimCopy.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(0.0, 0.0, 0.0f)];
positionAnimCopy.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
positionAnimCopy.additive = true;
positionAnimCopy.delegate = [[CATracingLayerAnimationDelegate alloc] initWithDelegate:anim.delegate animationStopped:^{
__strong CATracingLayer *strongSelf = weakSelf;
if (strongSelf != nil) {
[strongSelf invalidateUpTheTree];
}
}];
[self invalidateUpTheTree];
[self mirrorAnimationDownTheTree:animCopy key:@"sublayerTransform"];
[self mirrorPositionAnimationDownTheTree:positionAnimCopy key:@"sublayerTransform"];
} else if ([key isEqualToString:@"position"]) {
CABasicAnimation *animCopy = [anim copy];
CGPoint from = [animCopy.fromValue CGPointValue];
CGPoint to = [animCopy.toValue CGPointValue];
animCopy.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(to.x - from.x, to.y - from.y, 0.0f)];
animCopy.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
animCopy.keyPath = @"sublayerTransform";
__weak CATracingLayer *weakSelf = self;
anim.delegate = [[CATracingLayerAnimationDelegate alloc] initWithDelegate:anim.delegate animationStopped:^{
__strong CATracingLayer *strongSelf = weakSelf;
if (strongSelf != nil) {
[strongSelf invalidateUpTheTree];
}
}];
[super addAnimation:anim forKey:key];
CABasicAnimation *positionAnimCopy = [animCopy copy];
positionAnimCopy.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(-to.x + from.x, 0.0, 0.0f)];
positionAnimCopy.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
positionAnimCopy.additive = true;
positionAnimCopy.delegate = [[CATracingLayerAnimationDelegate alloc] initWithDelegate:anim.delegate animationStopped:^{
__strong CATracingLayer *strongSelf = weakSelf;
if (strongSelf != nil) {
[strongSelf invalidateUpTheTree];
}
}];
[self invalidateUpTheTree];
[self mirrorAnimationDownTheTree:animCopy key:@"sublayerTransform"];
[self mirrorPositionAnimationDownTheTree:positionAnimCopy key:@"sublayerTransform"];
} else if ([key isEqualToString:@"opacity"]) {
__weak CATracingLayer *weakSelf = self;
anim.delegate = [[CATracingLayerAnimationDelegate alloc] initWithDelegate:anim.delegate animationStopped:^{
__strong CATracingLayer *strongSelf = weakSelf;
if (strongSelf != nil) {
[strongSelf invalidateUpTheTree];
}
}];
[super addAnimation:anim forKey:key];
[self invalidateUpTheTree];
} else {
[super addAnimation:anim forKey:key];
}
} else {
[super addAnimation:anim forKey:key];
}
}
- (void)mirrorPositionAnimationDownTheTree:(CAAnimation *)animation key:(NSString *)key {
if ([animation isKindOfClass:[CABasicAnimation class]]) {
if ([((CABasicAnimation *)animation).keyPath isEqualToString:@"sublayerTransform"]) {
CALayer *positionAnimationMirrorTarget = [self animationMirrorTarget];
if (positionAnimationMirrorTarget != nil) {
[positionAnimationMirrorTarget addAnimation:[animation copy] forKey:key];
}
}
}
}
- (void)mirrorAnimationDownTheTree:(CAAnimation *)animation key:(NSString *)key {
for (CALayer *sublayer in self.sublayers) {
CATracingLayerInfo *traceableInfo = [sublayer traceableInfo];
if (traceableInfo != nil && traceableInfo.shouldBeAdjustedToInverseTransform) {
[sublayer addAnimation:[animation copy] forKey:key];
}
if ([sublayer isKindOfClass:[CATracingLayer class]]) {
[(CATracingLayer *)sublayer mirrorAnimationDownTheTree:animation key:key];
}
}
}
@end
@implementation CATracingLayerInfo
- (instancetype _Nonnull)initWithShouldBeAdjustedToInverseTransform:(bool)shouldBeAdjustedToInverseTransform userData:(id _Nullable)userData tracingTag:(int32_t)tracingTag disableChildrenTracingTags:(int32_t)disableChildrenTracingTags {
self = [super init];
if (self != nil) {
_shouldBeAdjustedToInverseTransform = shouldBeAdjustedToInverseTransform;
_userData = userData;
_tracingTag = tracingTag;
_disableChildrenTracingTags = disableChildrenTracingTags;
}
return self;
}
@end

View File

@ -0,0 +1,117 @@
import Foundation
import UIKit
private final class ChildWindowHostView: UIView, WindowHost {
var updateSize: ((CGSize) -> Void)?
var layoutSubviewsEvent: (() -> Void)?
var hitTestImpl: ((CGPoint, UIEvent?) -> UIView?)?
var presentController: ((ContainableController, PresentationSurfaceLevel, Bool, @escaping () -> Void) -> Void)?
var invalidateDeferScreenEdgeGestureImpl: (() -> Void)?
var invalidatePreferNavigationUIHiddenImpl: (() -> Void)?
var cancelInteractiveKeyboardGesturesImpl: (() -> Void)?
var forEachControllerImpl: (((ContainableController) -> Void) -> Void)?
var getAccessibilityElementsImpl: (() -> [Any]?)?
override var frame: CGRect {
didSet {
if self.frame.size != oldValue.size {
self.updateSize?(self.frame.size)
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
self.layoutSubviewsEvent?()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return self.hitTestImpl?(point, event)
}
func invalidateDeferScreenEdgeGestures() {
self.invalidateDeferScreenEdgeGestureImpl?()
}
func invalidatePreferNavigationUIHidden() {
self.invalidatePreferNavigationUIHiddenImpl?()
}
func cancelInteractiveKeyboardGestures() {
self.cancelInteractiveKeyboardGesturesImpl?()
}
func forEachController(_ f: (ContainableController) -> Void) {
self.forEachControllerImpl?(f)
}
func present(_ controller: ContainableController, on level: PresentationSurfaceLevel, blockInteraction: Bool, completion: @escaping () -> Void) {
self.presentController?(controller, level, blockInteraction, completion)
}
func presentInGlobalOverlay(_ controller: ContainableController) {
}
}
public func childWindowHostView(parent: UIView) -> WindowHostView {
let view = ChildWindowHostView()
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
let hostView = WindowHostView(containerView: view, eventView: view, isRotating: {
return false
}, updateSupportedInterfaceOrientations: { orientations in
}, updateDeferScreenEdgeGestures: { edges in
}, updatePreferNavigationUIHidden: { value in
})
view.updateSize = { [weak hostView] size in
hostView?.updateSize?(size, 0.0)
}
view.layoutSubviewsEvent = { [weak hostView] in
hostView?.layoutSubviews?()
}
/*window.updateIsUpdatingOrientationLayout = { [weak hostView] value in
hostView?.isUpdatingOrientationLayout = value
}
window.updateToInterfaceOrientation = { [weak hostView] in
hostView?.updateToInterfaceOrientation?()
}*/
view.presentController = { [weak hostView] controller, level, block, f in
hostView?.present?(controller, level, block, f)
}
/*view.presentNativeImpl = { [weak hostView] controller in
hostView?.presentNative?(controller)
}*/
view.hitTestImpl = { [weak hostView] point, event in
return hostView?.hitTest?(point, event)
}
view.invalidateDeferScreenEdgeGestureImpl = { [weak hostView] in
return hostView?.invalidateDeferScreenEdgeGesture?()
}
view.invalidatePreferNavigationUIHiddenImpl = { [weak hostView] in
return hostView?.invalidatePreferNavigationUIHidden?()
}
view.cancelInteractiveKeyboardGesturesImpl = { [weak hostView] in
hostView?.cancelInteractiveKeyboardGestures?()
}
view.forEachControllerImpl = { [weak hostView] f in
hostView?.forEachController?(f)
}
view.getAccessibilityElementsImpl = { [weak hostView] in
return hostView?.getAccessibilityElements?()
}
return hostView
}

View File

@ -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
}
}
}

View File

@ -0,0 +1,27 @@
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
public protocol PresentableController: class {
func viewDidAppear(completion: @escaping () -> Void)
}
public protocol ContainableController: class {
var view: UIView! { get }
var displayNode: ASDisplayNode { get }
var isViewLoaded: Bool { get }
var isOpaqueWhenInOverlay: Bool { get }
var blocksBackgroundWhenInOverlay: Bool { get }
var ready: Promise<Bool> { get }
func combinedSupportedOrientations(currentOrientationToLock: UIInterfaceOrientationMask) -> ViewControllerSupportedOrientations
var deferScreenEdgeGestures: UIRectEdge { get }
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition)
func updateToInterfaceOrientation(_ orientation: UIInterfaceOrientation)
func viewWillAppear(_ animated: Bool)
func viewWillDisappear(_ animated: Bool)
func viewDidAppear(_ animated: Bool)
func viewDidDisappear(_ animated: Bool)
}

View File

@ -0,0 +1,675 @@
import Foundation
import UIKit
import AsyncDisplayKit
public enum ContainedViewLayoutTransitionCurve {
case easeInOut
case spring
case custom(Float, Float, Float, Float)
}
public extension ContainedViewLayoutTransitionCurve {
var timingFunction: String {
switch self {
case .easeInOut:
return kCAMediaTimingFunctionEaseInEaseOut
case .spring:
return kCAMediaTimingFunctionSpring
case .custom:
return kCAMediaTimingFunctionEaseInEaseOut
}
}
var mediaTimingFunction: CAMediaTimingFunction? {
switch self {
case .easeInOut:
return nil
case .spring:
return nil
case let .custom(p1, p2, p3, p4):
return CAMediaTimingFunction(controlPoints: p1, p2, p3, p4)
}
}
#if os(iOS)
var viewAnimationOptions: UIViewAnimationOptions {
switch self {
case .easeInOut:
return [.curveEaseInOut]
case .spring:
return UIViewAnimationOptions(rawValue: 7 << 16)
case .custom:
return []
}
}
#endif
}
public enum ContainedViewLayoutTransition {
case immediate
case animated(duration: Double, curve: ContainedViewLayoutTransitionCurve)
public var isAnimated: Bool {
if case .immediate = self {
return false
} else {
return true
}
}
}
public extension ContainedViewLayoutTransition {
func updateFrame(node: ASDisplayNode, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) {
if node.frame.equalTo(frame) && !force {
completion?(true)
} else {
switch self {
case .immediate:
node.frame = frame
if let completion = completion {
completion(true)
}
case let .animated(duration, curve):
let previousFrame = node.frame
node.frame = frame
node.layer.animateFrame(from: previousFrame, to: frame, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, force: force, completion: { result in
if let completion = completion {
completion(result)
}
})
}
}
}
func updateBounds(node: ASDisplayNode, bounds: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) {
if node.bounds.equalTo(bounds) && !force {
completion?(true)
} else {
switch self {
case .immediate:
node.bounds = bounds
if let completion = completion {
completion(true)
}
case let .animated(duration, curve):
let previousBounds = node.bounds
node.bounds = bounds
node.layer.animateBounds(from: previousBounds, to: bounds, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, force: force, completion: { result in
if let completion = completion {
completion(result)
}
})
}
}
}
func updateBounds(layer: CALayer, bounds: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) {
if layer.bounds.equalTo(bounds) && !force {
completion?(true)
} else {
switch self {
case .immediate:
layer.bounds = bounds
if let completion = completion {
completion(true)
}
case let .animated(duration, curve):
let previousBounds = layer.bounds
layer.bounds = bounds
layer.animateBounds(from: previousBounds, to: bounds, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, force: force, completion: { result in
if let completion = completion {
completion(result)
}
})
}
}
}
func updatePosition(node: ASDisplayNode, position: CGPoint, completion: ((Bool) -> Void)? = nil) {
if node.position.equalTo(position) {
completion?(true)
} else {
switch self {
case .immediate:
node.position = position
if let completion = completion {
completion(true)
}
case let .animated(duration, curve):
let previousPosition = node.position
node.position = position
node.layer.animatePosition(from: previousPosition, to: position, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
if let completion = completion {
completion(result)
}
})
}
}
}
func updatePosition(layer: CALayer, position: CGPoint, completion: ((Bool) -> Void)? = nil) {
if layer.position.equalTo(position) {
completion?(true)
} else {
switch self {
case .immediate:
layer.position = position
if let completion = completion {
completion(true)
}
case let .animated(duration, curve):
let previousPosition = layer.position
layer.position = position
layer.animatePosition(from: previousPosition, to: position, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
if let completion = completion {
completion(result)
}
})
}
}
}
func animatePosition(node: ASDisplayNode, from position: CGPoint, completion: ((Bool) -> Void)? = nil) {
switch self {
case .immediate:
if let completion = completion {
completion(true)
}
case let .animated(duration, curve):
node.layer.animatePosition(from: position, to: node.position, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
if let completion = completion {
completion(result)
}
})
}
}
func animatePosition(node: ASDisplayNode, to position: CGPoint, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
if node.position.equalTo(position) {
completion?(true)
} else {
switch self {
case .immediate:
if let completion = completion {
completion(true)
}
case let .animated(duration, curve):
node.layer.animatePosition(from: node.position, to: position, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: { result in
if let completion = completion {
completion(result)
}
})
}
}
}
func animateFrame(node: ASDisplayNode, from frame: CGRect, to toFrame: CGRect? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
switch self {
case .immediate:
if let completion = completion {
completion(true)
}
case let .animated(duration, curve):
node.layer.animateFrame(from: frame, to: toFrame ?? node.layer.frame, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: { result in
if let completion = completion {
completion(result)
}
})
}
}
func animateBounds(layer: CALayer, from bounds: CGRect, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
switch self {
case .immediate:
if let completion = completion {
completion(true)
}
case let .animated(duration, curve):
layer.animateBounds(from: bounds, to: layer.bounds, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: { result in
if let completion = completion {
completion(result)
}
})
}
}
func animateOffsetAdditive(node: ASDisplayNode, offset: CGFloat) {
switch self {
case .immediate:
break
case let .animated(duration, curve):
node.layer.animateBoundsOriginYAdditive(from: offset, to: 0.0, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction)
}
}
func animateHorizontalOffsetAdditive(node: ASDisplayNode, offset: CGFloat) {
switch self {
case .immediate:
break
case let .animated(duration, curve):
node.layer.animateBoundsOriginXAdditive(from: offset, to: 0.0, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction)
}
}
func animateOffsetAdditive(layer: CALayer, offset: CGFloat, completion: (() -> Void)? = nil) {
switch self {
case .immediate:
completion?()
case let .animated(duration, curve):
layer.animateBoundsOriginYAdditive(from: offset, to: 0.0, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { _ in
completion?()
})
}
}
func animatePositionAdditive(node: ASDisplayNode, offset: CGFloat, removeOnCompletion: Bool = true, completion: @escaping (Bool) -> Void) {
switch self {
case .immediate:
break
case let .animated(duration, curve):
node.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion)
}
}
func animatePositionAdditive(layer: CALayer, offset: CGFloat, removeOnCompletion: Bool = true, completion: @escaping (Bool) -> Void) {
switch self {
case .immediate:
break
case let .animated(duration, curve):
layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion)
}
}
func animatePositionAdditive(node: ASDisplayNode, offset: CGPoint, removeOnCompletion: Bool = true, completion: (() -> Void)? = nil) {
switch self {
case .immediate:
break
case let .animated(duration, curve):
node.layer.animatePosition(from: offset, to: CGPoint(), duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: { _ in
completion?()
})
}
}
func animatePositionAdditive(layer: CALayer, offset: CGPoint, to toOffset: CGPoint = CGPoint(), removeOnCompletion: Bool = true, completion: (() -> Void)? = nil) {
switch self {
case .immediate:
break
case let .animated(duration, curve):
layer.animatePosition(from: offset, to: toOffset, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: { _ in
completion?()
})
}
}
func updateFrame(view: UIView, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) {
if view.frame.equalTo(frame) && !force {
completion?(true)
} else {
switch self {
case .immediate:
view.frame = frame
if let completion = completion {
completion(true)
}
case let .animated(duration, curve):
let previousFrame = view.frame
view.frame = frame
view.layer.animateFrame(from: previousFrame, to: frame, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, force: force, completion: { result in
if let completion = completion {
completion(result)
}
})
}
}
}
func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)? = nil) {
if layer.frame.equalTo(frame) {
completion?(true)
} else {
switch self {
case .immediate:
layer.frame = frame
if let completion = completion {
completion(true)
}
case let .animated(duration, curve):
let previousFrame = layer.frame
layer.frame = frame
layer.animateFrame(from: previousFrame, to: frame, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
if let completion = completion {
completion(result)
}
})
}
}
}
func updateAlpha(node: ASDisplayNode, alpha: CGFloat, completion: ((Bool) -> Void)? = nil) {
if node.alpha.isEqual(to: alpha) {
if let completion = completion {
completion(true)
}
return
}
switch self {
case .immediate:
node.alpha = alpha
if let completion = completion {
completion(true)
}
case let .animated(duration, curve):
let previousAlpha = node.alpha
node.alpha = alpha
node.layer.animateAlpha(from: previousAlpha, to: alpha, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
if let completion = completion {
completion(result)
}
})
}
}
func updateAlpha(layer: CALayer, alpha: CGFloat, completion: ((Bool) -> Void)? = nil) {
if layer.opacity.isEqual(to: Float(alpha)) {
if let completion = completion {
completion(true)
}
return
}
switch self {
case .immediate:
layer.opacity = Float(alpha)
if let completion = completion {
completion(true)
}
case let .animated(duration, curve):
let previousAlpha = layer.opacity
layer.opacity = Float(alpha)
layer.animateAlpha(from: CGFloat(previousAlpha), to: alpha, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
if let completion = completion {
completion(result)
}
})
}
}
func updateBackgroundColor(node: ASDisplayNode, color: UIColor, completion: ((Bool) -> Void)? = nil) {
if let nodeColor = node.backgroundColor, nodeColor.isEqual(color) {
if let completion = completion {
completion(true)
}
return
}
switch self {
case .immediate:
node.backgroundColor = color
if let completion = completion {
completion(true)
}
case let .animated(duration, curve):
if let nodeColor = node.backgroundColor {
node.backgroundColor = color
node.layer.animate(from: nodeColor.cgColor, to: color.cgColor, keyPath: "backgroundColor", timingFunction: curve.timingFunction, duration: duration, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
if let completion = completion {
completion(result)
}
})
} else {
node.backgroundColor = color
if let completion = completion {
completion(true)
}
}
}
}
func updateCornerRadius(node: ASDisplayNode, cornerRadius: CGFloat, completion: ((Bool) -> Void)? = nil) {
if node.cornerRadius.isEqual(to: cornerRadius) {
if let completion = completion {
completion(true)
}
return
}
switch self {
case .immediate:
node.cornerRadius = cornerRadius
if let completion = completion {
completion(true)
}
case let .animated(duration, curve):
let previousCornerRadius = node.cornerRadius
node.cornerRadius = cornerRadius
node.layer.animate(from: NSNumber(value: Float(previousCornerRadius)), to: NSNumber(value: Float(cornerRadius)), keyPath: "cornerRadius", timingFunction: curve.timingFunction, duration: duration, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
if let completion = completion {
completion(result)
}
})
}
}
func animateTransformScale(node: ASDisplayNode, from fromScale: CGFloat, completion: ((Bool) -> Void)? = nil) {
let t = node.layer.transform
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
if currentScale.isEqual(to: fromScale) {
if let completion = completion {
completion(true)
}
return
}
switch self {
case .immediate:
if let completion = completion {
completion(true)
}
case let .animated(duration, curve):
node.layer.animateScale(from: fromScale, to: currentScale, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
if let completion = completion {
completion(result)
}
})
}
}
func updateTransformScale(node: ASDisplayNode, scale: CGFloat, completion: ((Bool) -> Void)? = nil) {
let t = node.layer.transform
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
if currentScale.isEqual(to: scale) {
if let completion = completion {
completion(true)
}
return
}
switch self {
case .immediate:
node.layer.transform = CATransform3DMakeScale(scale, scale, 1.0)
if let completion = completion {
completion(true)
}
case let .animated(duration, curve):
node.layer.transform = CATransform3DMakeScale(scale, scale, 1.0)
node.layer.animateScale(from: currentScale, to: scale, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
if let completion = completion {
completion(result)
}
})
}
}
func updateTransformScale(layer: CALayer, scale: CGFloat, completion: ((Bool) -> Void)? = nil) {
let t = layer.transform
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
if currentScale.isEqual(to: scale) {
if let completion = completion {
completion(true)
}
return
}
switch self {
case .immediate:
layer.transform = CATransform3DMakeScale(scale, scale, 1.0)
if let completion = completion {
completion(true)
}
case let .animated(duration, curve):
layer.transform = CATransform3DMakeScale(scale, scale, 1.0)
layer.animateScale(from: currentScale, to: scale, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
if let completion = completion {
completion(result)
}
})
}
}
func updateSublayerTransformScale(node: ASDisplayNode, scale: CGFloat, completion: ((Bool) -> Void)? = nil) {
if !node.isNodeLoaded {
node.subnodeTransform = CATransform3DMakeScale(scale, scale, 1.0)
return
}
let t = node.layer.sublayerTransform
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
if currentScale.isEqual(to: scale) {
if let completion = completion {
completion(true)
}
return
}
switch self {
case .immediate:
node.layer.sublayerTransform = CATransform3DMakeScale(scale, scale, 1.0)
if let completion = completion {
completion(true)
}
case let .animated(duration, curve):
node.layer.sublayerTransform = CATransform3DMakeScale(scale, scale, 1.0)
node.layer.animate(from: NSValue(caTransform3D: t), to: NSValue(caTransform3D: node.layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: {
result in
if let completion = completion {
completion(result)
}
})
}
}
func updateSublayerTransformScale(node: ASDisplayNode, scale: CGPoint, completion: ((Bool) -> Void)? = nil) {
if !node.isNodeLoaded {
node.subnodeTransform = CATransform3DMakeScale(scale.x, scale.y, 1.0)
return
}
let t = node.layer.sublayerTransform
let currentScaleX = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
var currentScaleY = sqrt((t.m21 * t.m21) + (t.m22 * t.m22) + (t.m23 * t.m23))
if t.m22 < 0.0 {
currentScaleY = -currentScaleY
}
if CGPoint(x: currentScaleX, y: currentScaleY) == scale {
if let completion = completion {
completion(true)
}
return
}
switch self {
case .immediate:
node.layer.sublayerTransform = CATransform3DMakeScale(scale.x, scale.y, 1.0)
if let completion = completion {
completion(true)
}
case let .animated(duration, curve):
node.layer.sublayerTransform = CATransform3DMakeScale(scale.x, scale.y, 1.0)
node.layer.animate(from: NSValue(caTransform3D: t), to: NSValue(caTransform3D: node.layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: {
result in
if let completion = completion {
completion(result)
}
})
}
}
func updateTransformScale(node: ASDisplayNode, scale: CGPoint, completion: ((Bool) -> Void)? = nil) {
if !node.isNodeLoaded {
node.subnodeTransform = CATransform3DMakeScale(scale.x, scale.y, 1.0)
return
}
let t = node.layer.transform
let currentScaleX = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
var currentScaleY = sqrt((t.m21 * t.m21) + (t.m22 * t.m22) + (t.m23 * t.m23))
if t.m22 < 0.0 {
currentScaleY = -currentScaleY
}
if CGPoint(x: currentScaleX, y: currentScaleY) == scale {
if let completion = completion {
completion(true)
}
return
}
switch self {
case .immediate:
node.layer.transform = CATransform3DMakeScale(scale.x, scale.y, 1.0)
if let completion = completion {
completion(true)
}
case let .animated(duration, curve):
node.layer.transform = CATransform3DMakeScale(scale.x, scale.y, 1.0)
node.layer.animate(from: NSValue(caTransform3D: t), to: NSValue(caTransform3D: node.layer.transform), keyPath: "transform", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: {
result in
if let completion = completion {
completion(result)
}
})
}
}
func updateSublayerTransformOffset(layer: CALayer, offset: CGPoint, completion: ((Bool) -> Void)? = nil) {
let t = layer.transform
let currentOffset = CGPoint(x: t.m41, y: t.m42)
if currentOffset == offset {
if let completion = completion {
completion(true)
}
return
}
switch self {
case .immediate:
layer.sublayerTransform = CATransform3DMakeTranslation(offset.x, offset.y, 0.0)
if let completion = completion {
completion(true)
}
case let .animated(duration, curve):
layer.sublayerTransform = CATransform3DMakeTranslation(offset.x, offset.y, 0.0)
layer.animate(from: NSValue(caTransform3D: t), to: NSValue(caTransform3D: layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: {
result in
if let completion = completion {
completion(result)
}
})
}
}
}
#if os(iOS)
public extension ContainedViewLayoutTransition {
public func animateView(_ f: @escaping () -> Void) {
switch self {
case .immediate:
f()
case let .animated(duration, curve):
UIView.animate(withDuration: duration, delay: 0.0, options: curve.viewAnimationOptions, animations: {
f()
}, completion: nil)
}
}
}
#endif

View File

@ -0,0 +1,87 @@
import UIKit
public struct ContainerViewLayoutInsetOptions: OptionSet {
public let rawValue: Int
public init(rawValue: Int) {
self.rawValue = rawValue
}
public init() {
self.rawValue = 0
}
public static let statusBar = ContainerViewLayoutInsetOptions(rawValue: 1 << 0)
public static let input = ContainerViewLayoutInsetOptions(rawValue: 1 << 1)
}
public enum ContainerViewLayoutSizeClass {
case compact
case regular
}
public struct LayoutMetrics: Equatable {
public let widthClass: ContainerViewLayoutSizeClass
public let heightClass: ContainerViewLayoutSizeClass
public init(widthClass: ContainerViewLayoutSizeClass, heightClass: ContainerViewLayoutSizeClass) {
self.widthClass = widthClass
self.heightClass = heightClass
}
public init() {
self.widthClass = .compact
self.heightClass = .compact
}
}
public struct ContainerViewLayout: Equatable {
public let size: CGSize
public let metrics: LayoutMetrics
public let intrinsicInsets: UIEdgeInsets
public let safeInsets: UIEdgeInsets
public let statusBarHeight: CGFloat?
public var inputHeight: CGFloat?
public let standardInputHeight: CGFloat
public let inputHeightIsInteractivellyChanging: Bool
public let inVoiceOver: Bool
public init(size: CGSize, metrics: LayoutMetrics, intrinsicInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, statusBarHeight: CGFloat?, inputHeight: CGFloat?, standardInputHeight: CGFloat, inputHeightIsInteractivellyChanging: Bool, inVoiceOver: Bool) {
self.size = size
self.metrics = metrics
self.intrinsicInsets = intrinsicInsets
self.safeInsets = safeInsets
self.statusBarHeight = statusBarHeight
self.inputHeight = inputHeight
self.standardInputHeight = standardInputHeight
self.inputHeightIsInteractivellyChanging = inputHeightIsInteractivellyChanging
self.inVoiceOver = inVoiceOver
}
public func insets(options: ContainerViewLayoutInsetOptions) -> UIEdgeInsets {
var insets = self.intrinsicInsets
if let statusBarHeight = self.statusBarHeight , options.contains(.statusBar) {
insets.top += statusBarHeight
}
if let inputHeight = self.inputHeight , options.contains(.input) {
insets.bottom = max(inputHeight, insets.bottom)
}
return insets
}
public func addedInsets(insets: UIEdgeInsets) -> ContainerViewLayout {
return ContainerViewLayout(size: self.size, metrics: self.metrics, intrinsicInsets: UIEdgeInsets(top: self.intrinsicInsets.top + insets.top, left: self.intrinsicInsets.left + insets.left, bottom: self.intrinsicInsets.bottom + insets.bottom, right: self.intrinsicInsets.right + insets.right), safeInsets: self.safeInsets, statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight, standardInputHeight: self.standardInputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging, inVoiceOver: self.inVoiceOver)
}
public func withUpdatedSize(_ size: CGSize) -> ContainerViewLayout {
return ContainerViewLayout(size: size, metrics: self.metrics, intrinsicInsets: self.intrinsicInsets, safeInsets: self.safeInsets, statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight, standardInputHeight: self.standardInputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging, inVoiceOver: self.inVoiceOver)
}
public func withUpdatedInputHeight(_ inputHeight: CGFloat?) -> ContainerViewLayout {
return ContainerViewLayout(size: self.size, metrics: self.metrics, intrinsicInsets: self.intrinsicInsets, safeInsets: self.safeInsets, statusBarHeight: self.statusBarHeight, inputHeight: inputHeight, standardInputHeight: self.standardInputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging, inVoiceOver: self.inVoiceOver)
}
public func withUpdatedMetrics(_ metrics: LayoutMetrics) -> ContainerViewLayout {
return ContainerViewLayout(size: self.size, metrics: metrics, intrinsicInsets: self.intrinsicInsets, safeInsets: self.safeInsets, statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight, standardInputHeight: self.standardInputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging, inVoiceOver: self.inVoiceOver)
}
}

View File

@ -0,0 +1,16 @@
import UIKit
public enum ContextMenuActionContent {
case text(title: String, accessibilityLabel: String)
case icon(UIImage)
}
public struct ContextMenuAction {
public let content: ContextMenuActionContent
public let action: () -> Void
public init(content: ContextMenuActionContent, action: @escaping () -> Void) {
self.content = content
self.action = action
}
}

View File

@ -0,0 +1,118 @@
import Foundation
import UIKit
import AsyncDisplayKit
final private class ContextMenuActionButton: HighlightTrackingButton {
override func convert(_ point: CGPoint, from view: UIView?) -> CGPoint {
if view is UIWindow {
return super.convert(point, from: nil)
} else {
return super.convert(point, from: view)
}
}
}
final class ContextMenuActionNode: ASDisplayNode {
private let textNode: ImmediateTextNode?
private var textSize: CGSize?
private let iconNode: ASImageNode?
private let action: () -> Void
private let button: ContextMenuActionButton
private let actionArea: AccessibilityAreaNode
var dismiss: (() -> Void)?
init(action: ContextMenuAction) {
self.actionArea = AccessibilityAreaNode()
self.actionArea.accessibilityTraits = UIAccessibilityTraitButton
switch action.content {
case let .text(title, accessibilityLabel):
self.actionArea.accessibilityLabel = accessibilityLabel
let textNode = ImmediateTextNode()
textNode.isUserInteractionEnabled = false
textNode.displaysAsynchronously = false
textNode.attributedText = NSAttributedString(string: title, font: Font.regular(14.0), textColor: UIColor.white)
textNode.isAccessibilityElement = false
self.textNode = textNode
self.iconNode = nil
case let .icon(image):
let iconNode = ASImageNode()
iconNode.displaysAsynchronously = false
iconNode.displayWithoutProcessing = true
iconNode.image = image
self.iconNode = iconNode
self.textNode = nil
}
self.action = action.action
self.button = ContextMenuActionButton()
self.button.isAccessibilityElement = false
super.init()
self.backgroundColor = UIColor(white: 0.0, alpha: 0.8)
if let textNode = self.textNode {
self.addSubnode(textNode)
}
if let iconNode = self.iconNode {
self.addSubnode(iconNode)
}
self.button.highligthedChanged = { [weak self] highlighted in
self?.backgroundColor = highlighted ? UIColor(white: 0.0, alpha: 0.4) : UIColor(white: 0.0, alpha: 0.8)
}
self.view.addSubview(self.button)
self.addSubnode(self.actionArea)
self.actionArea.activate = { [weak self] in
self?.buttonPressed()
return true
}
}
override func didLoad() {
super.didLoad()
self.button.addTarget(self, action: #selector(self.buttonPressed), for: [.touchUpInside])
}
@objc private func buttonPressed() {
self.backgroundColor = UIColor(white: 0.0, alpha: 0.4)
self.action()
if let dismiss = self.dismiss {
dismiss()
}
}
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
if let textNode = self.textNode {
let textSize = textNode.updateLayout(constrainedSize)
self.textSize = textSize
return CGSize(width: textSize.width + 36.0, height: 54.0)
} else if let iconNode = self.iconNode, let image = iconNode.image {
return CGSize(width: image.size.width + 36.0, height: 54.0)
} else {
return CGSize(width: 36.0, height: 54.0)
}
}
override func layout() {
super.layout()
self.button.frame = self.bounds
self.actionArea.frame = self.bounds
if let textNode = self.textNode, let textSize = self.textSize {
textNode.frame = CGRect(origin: CGPoint(x: floor((self.bounds.size.width - textSize.width) / 2.0), y: floor((self.bounds.size.height - textSize.height) / 2.0)), size: textSize)
}
if let iconNode = self.iconNode, let image = iconNode.image {
let iconSize = image.size
iconNode.frame = CGRect(origin: CGPoint(x: floor((self.bounds.size.width - iconSize.width) / 2.0), y: floor((self.bounds.size.height - iconSize.height) / 2.0)), size: iconSize)
}
}
}

View File

@ -0,0 +1,90 @@
import Foundation
import UIKit
import AsyncDisplayKit
private struct CachedMaskParams: Equatable {
let size: CGSize
let relativeArrowPosition: CGFloat
let arrowOnBottom: Bool
}
private final class ContextMenuContainerMaskView: UIView {
override class var layerClass: AnyClass {
return CAShapeLayer.self
}
}
public final class ContextMenuContainerNode: ASDisplayNode {
private var cachedMaskParams: CachedMaskParams?
private let maskView = ContextMenuContainerMaskView()
public var relativeArrowPosition: (CGFloat, Bool)?
//private let effectView: UIVisualEffectView
override public init() {
//self.effectView = UIVisualEffectView(effect: UIBlurEffect(style: .light))
super.init()
self.backgroundColor = UIColor(rgb: 0xeaecec)
//self.view.addSubview(self.effectView)
//self.effectView.mask = self.maskView
self.view.mask = self.maskView
}
override public func didLoad() {
super.didLoad()
self.layer.allowsGroupOpacity = true
}
override public func layout() {
super.layout()
self.updateLayout(transition: .immediate)
}
public func updateLayout(transition: ContainedViewLayoutTransition) {
//self.effectView.frame = self.bounds
let maskParams = CachedMaskParams(size: self.bounds.size, relativeArrowPosition: self.relativeArrowPosition?.0 ?? self.bounds.size.width / 2.0, arrowOnBottom: self.relativeArrowPosition?.1 ?? true)
if self.cachedMaskParams != maskParams {
let path = UIBezierPath()
let cornerRadius: CGFloat = 6.0
let verticalInset: CGFloat = 9.0
let arrowWidth: CGFloat = 18.0
let requestedArrowPosition = maskParams.relativeArrowPosition
let arrowPosition = max(cornerRadius + arrowWidth / 2.0, min(maskParams.size.width - cornerRadius - arrowWidth / 2.0, requestedArrowPosition))
let arrowOnBottom = maskParams.arrowOnBottom
path.move(to: CGPoint(x: 0.0, y: verticalInset + cornerRadius))
path.addArc(withCenter: CGPoint(x: cornerRadius, y: verticalInset + cornerRadius), radius: cornerRadius, startAngle: CGFloat.pi, endAngle: CGFloat(3.0 * CGFloat.pi / 2.0), clockwise: true)
if !arrowOnBottom {
path.addLine(to: CGPoint(x: arrowPosition - arrowWidth / 2.0, y: verticalInset))
path.addLine(to: CGPoint(x: arrowPosition, y: 0.0))
path.addLine(to: CGPoint(x: arrowPosition + arrowWidth / 2.0, y: verticalInset))
}
path.addLine(to: CGPoint(x: maskParams.size.width - cornerRadius, y: verticalInset))
path.addArc(withCenter: CGPoint(x: maskParams.size.width - cornerRadius, y: verticalInset + cornerRadius), radius: cornerRadius, startAngle: CGFloat(3.0 * CGFloat.pi / 2.0), endAngle: 0.0, clockwise: true)
path.addLine(to: CGPoint(x: maskParams.size.width, y: maskParams.size.height - cornerRadius - verticalInset))
path.addArc(withCenter: CGPoint(x: maskParams.size.width - cornerRadius, y: maskParams.size.height - cornerRadius - verticalInset), radius: cornerRadius, startAngle: 0.0, endAngle: CGFloat(CGFloat.pi / 2.0), clockwise: true)
if arrowOnBottom {
path.addLine(to: CGPoint(x: arrowPosition + arrowWidth / 2.0, y: maskParams.size.height - verticalInset))
path.addLine(to: CGPoint(x: arrowPosition, y: maskParams.size.height))
path.addLine(to: CGPoint(x: arrowPosition - arrowWidth / 2.0, y: maskParams.size.height - verticalInset))
}
path.addLine(to: CGPoint(x: cornerRadius, y: maskParams.size.height - verticalInset))
path.addArc(withCenter: CGPoint(x: cornerRadius, y: maskParams.size.height - cornerRadius - verticalInset), radius: cornerRadius, startAngle: CGFloat(CGFloat.pi / 2.0), endAngle: CGFloat(M_PI), clockwise: true)
path.close()
self.cachedMaskParams = maskParams
if let layer = self.maskView.layer as? CAShapeLayer {
if case let .animated(duration, curve) = transition, let previousPath = layer.path {
layer.animate(from: previousPath, to: path.cgPath, keyPath: "path", timingFunction: curve.timingFunction, duration: duration)
}
layer.path = path.cgPath
}
}
}
}

View File

@ -0,0 +1,96 @@
import Foundation
import UIKit
import AsyncDisplayKit
public final class ContextMenuControllerPresentationArguments {
fileprivate let sourceNodeAndRect: () -> (ASDisplayNode, CGRect, ASDisplayNode, CGRect)?
public init(sourceNodeAndRect: @escaping () -> (ASDisplayNode, CGRect, ASDisplayNode, CGRect)?) {
self.sourceNodeAndRect = sourceNodeAndRect
}
}
public final class ContextMenuController: ViewController, KeyShortcutResponder {
private var contextMenuNode: ContextMenuNode {
return self.displayNode as! ContextMenuNode
}
public var keyShortcuts: [KeyShortcut] {
return [KeyShortcut(input: UIKeyInputEscape, action: { [weak self] in
if let strongSelf = self {
strongSelf.dismiss()
}
})]
}
private let actions: [ContextMenuAction]
private let catchTapsOutside: Bool
private let hasHapticFeedback: Bool
private var layout: ContainerViewLayout?
public var dismissed: (() -> Void)?
public init(actions: [ContextMenuAction], catchTapsOutside: Bool = false, hasHapticFeedback: Bool = false) {
self.actions = actions
self.catchTapsOutside = catchTapsOutside
self.hasHapticFeedback = hasHapticFeedback
super.init(navigationBarPresentationData: nil)
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func loadDisplayNode() {
self.displayNode = ContextMenuNode(actions: self.actions, dismiss: { [weak self] in
self?.dismissed?()
self?.contextMenuNode.animateOut {
self?.presentingViewController?.dismiss(animated: false)
}
}, catchTapsOutside: self.catchTapsOutside, hasHapticFeedback: self.hasHapticFeedback)
self.displayNodeDidLoad()
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.contextMenuNode.animateIn()
}
override public func dismiss(completion: (() -> Void)? = nil) {
self.dismissed?()
self.contextMenuNode.animateOut { [weak self] in
self?.presentingViewController?.dismiss(animated: false)
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
if self.layout != nil && self.layout! != layout {
self.dismissed?()
self.contextMenuNode.animateOut { [weak self] in
self?.presentingViewController?.dismiss(animated: false)
}
} else {
self.layout = layout
if let presentationArguments = self.presentationArguments as? ContextMenuControllerPresentationArguments, let (sourceNode, sourceRect, containerNode, containerRect) = presentationArguments.sourceNodeAndRect() {
self.contextMenuNode.sourceRect = sourceNode.view.convert(sourceRect, to: nil)
self.contextMenuNode.containerRect = containerNode.view.convert(containerRect, to: nil)
} else {
self.contextMenuNode.sourceRect = nil
self.contextMenuNode.containerRect = nil
}
self.contextMenuNode.containerLayoutUpdated(layout, transition: transition)
}
}
override public func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.contextMenuNode.animateIn()
}
}

View File

@ -0,0 +1,264 @@
import Foundation
import UIKit
import AsyncDisplayKit
private func generateShadowImage() -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 1.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setShadow(offset: CGSize(), blur: 10.0, color: UIColor(white: 0.18, alpha: 1.0).cgColor)
context.setFillColor(UIColor(white: 0.18, alpha: 1.0).cgColor)
context.fill(CGRect(origin: CGPoint(x: -15.0, y: 0.0), size: CGSize(width: 30.0, height: 1.0)))
})
}
private final class ContextMenuContentScrollNode: ASDisplayNode {
var contentWidth: CGFloat = 0.0
private var initialOffset: CGFloat = 0.0
private let leftShadow: ASImageNode
private let rightShadow: ASImageNode
private let leftOverscrollNode: ASDisplayNode
private let rightOverscrollNode: ASDisplayNode
let contentNode: ASDisplayNode
override init() {
self.contentNode = ASDisplayNode()
let shadowImage = generateShadowImage()
self.leftShadow = ASImageNode()
self.leftShadow.displayWithoutProcessing = true
self.leftShadow.displaysAsynchronously = false
self.leftShadow.image = shadowImage
self.rightShadow = ASImageNode()
self.rightShadow.displayWithoutProcessing = true
self.rightShadow.displaysAsynchronously = false
self.rightShadow.image = shadowImage
self.rightShadow.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
self.leftOverscrollNode = ASDisplayNode()
self.leftOverscrollNode.backgroundColor = UIColor(white: 0.0, alpha: 0.8)
self.rightOverscrollNode = ASDisplayNode()
self.rightOverscrollNode.backgroundColor = UIColor(white: 0.0, alpha: 0.8)
super.init()
self.contentNode.addSubnode(self.leftOverscrollNode)
self.contentNode.addSubnode(self.rightOverscrollNode)
self.addSubnode(self.contentNode)
self.addSubnode(self.leftShadow)
self.addSubnode(self.rightShadow)
}
override func didLoad() {
super.didLoad()
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
self.view.addGestureRecognizer(panRecognizer)
}
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
self.initialOffset = self.contentNode.bounds.origin.x
case .changed:
var bounds = self.contentNode.bounds
bounds.origin.x = self.initialOffset - recognizer.translation(in: self.view).x
if bounds.origin.x > self.contentWidth - bounds.size.width {
let delta = bounds.origin.x - (self.contentWidth - bounds.size.width)
bounds.origin.x = self.contentWidth - bounds.size.width + ((1.0 - (1.0 / (((delta) * 0.55 / (50.0)) + 1.0))) * 50.0)
}
if bounds.origin.x < 0.0 {
let delta = -bounds.origin.x
bounds.origin.x = -((1.0 - (1.0 / (((delta) * 0.55 / (50.0)) + 1.0))) * 50.0)
}
self.contentNode.bounds = bounds
self.updateShadows(.immediate)
case .ended, .cancelled:
var bounds = self.contentNode.bounds
bounds.origin.x = self.initialOffset - recognizer.translation(in: self.view).x
var duration = 0.4
if abs(bounds.origin.x - self.initialOffset) > 10.0 || abs(recognizer.velocity(in: self.view).x) > 100.0 {
duration = 0.2
if bounds.origin.x < self.initialOffset {
bounds.origin.x = 0.0
} else {
bounds.origin.x = self.contentWidth - bounds.size.width
}
} else {
bounds.origin.x = self.initialOffset
}
if bounds.origin.x > self.contentWidth - bounds.size.width {
bounds.origin.x = self.contentWidth - bounds.size.width
}
if bounds.origin.x < 0.0 {
bounds.origin.x = 0.0
}
let previousBounds = self.contentNode.bounds
self.contentNode.bounds = bounds
self.contentNode.layer.animateBounds(from: previousBounds, to: bounds, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
self.updateShadows(.animated(duration: duration, curve: .spring))
default:
break
}
}
override func layout() {
let bounds = self.bounds
self.contentNode.frame = bounds
self.leftShadow.frame = CGRect(origin: CGPoint(), size: CGSize(width: 30.0, height: bounds.height))
self.rightShadow.frame = CGRect(origin: CGPoint(x: bounds.size.width - 30.0, y: 0.0), size: CGSize(width: 30.0, height: bounds.height))
self.leftOverscrollNode.frame = bounds.offsetBy(dx: -bounds.width, dy: 0.0)
self.rightOverscrollNode.frame = bounds.offsetBy(dx: self.contentWidth, dy: 0.0)
self.updateShadows(.immediate)
}
private func updateShadows(_ transition: ContainedViewLayoutTransition) {
let bounds = self.contentNode.bounds
let leftAlpha = max(0.0, min(1.0, bounds.minX / 20.0))
transition.updateAlpha(node: self.leftShadow, alpha: leftAlpha)
let rightAlpha = max(0.0, min(1.0, (self.contentWidth - bounds.maxX) / 20.0))
transition.updateAlpha(node: self.rightShadow, alpha: rightAlpha)
}
}
final class ContextMenuNode: ASDisplayNode {
private let actions: [ContextMenuAction]
private let dismiss: () -> Void
private let containerNode: ContextMenuContainerNode
private let scrollNode: ContextMenuContentScrollNode
private let actionNodes: [ContextMenuActionNode]
var sourceRect: CGRect?
var containerRect: CGRect?
var arrowOnBottom: Bool = true
private var dismissedByTouchOutside = false
private let catchTapsOutside: Bool
private let feedback: HapticFeedback?
init(actions: [ContextMenuAction], dismiss: @escaping () -> Void, catchTapsOutside: Bool, hasHapticFeedback: Bool = false) {
self.actions = actions
self.dismiss = dismiss
self.catchTapsOutside = catchTapsOutside
self.containerNode = ContextMenuContainerNode()
self.scrollNode = ContextMenuContentScrollNode()
self.actionNodes = actions.map { action in
return ContextMenuActionNode(action: action)
}
if hasHapticFeedback {
self.feedback = HapticFeedback()
self.feedback?.prepareImpact(.light)
} else {
self.feedback = nil
}
super.init()
self.containerNode.addSubnode(self.scrollNode)
self.addSubnode(self.containerNode)
let dismissNode = {
dismiss()
}
for actionNode in self.actionNodes {
actionNode.dismiss = dismissNode
self.scrollNode.contentNode.addSubnode(actionNode)
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
var unboundActionsWidth: CGFloat = 0.0
let actionSeparatorWidth: CGFloat = UIScreenPixel
for actionNode in self.actionNodes {
if !unboundActionsWidth.isZero {
unboundActionsWidth += actionSeparatorWidth
}
let actionSize = actionNode.measure(CGSize(width: layout.size.width, height: 54.0))
actionNode.frame = CGRect(origin: CGPoint(x: unboundActionsWidth, y: 0.0), size: actionSize)
unboundActionsWidth += actionSize.width
}
let maxActionsWidth = layout.size.width - 20.0
let actionsWidth = min(unboundActionsWidth, maxActionsWidth)
let sourceRect: CGRect = self.sourceRect ?? CGRect(origin: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0), size: CGSize())
let containerRect: CGRect = self.containerRect ?? self.bounds
let insets = layout.insets(options: [.statusBar, .input])
let verticalOrigin: CGFloat
var arrowOnBottom = true
if sourceRect.minY - 54.0 > containerRect.minY + insets.top {
verticalOrigin = sourceRect.minY - 54.0
} else {
verticalOrigin = min(containerRect.maxY - insets.bottom - 54.0, sourceRect.maxY)
arrowOnBottom = false
}
self.arrowOnBottom = arrowOnBottom
let horizontalOrigin: CGFloat = floor(max(8.0, min(max(sourceRect.minX + 8.0, sourceRect.midX - actionsWidth / 2.0), layout.size.width - actionsWidth - 8.0)))
self.containerNode.frame = CGRect(origin: CGPoint(x: horizontalOrigin, y: verticalOrigin), size: CGSize(width: actionsWidth, height: 54.0))
self.containerNode.relativeArrowPosition = (sourceRect.midX - horizontalOrigin, arrowOnBottom)
self.scrollNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: actionsWidth, height: 54.0))
self.scrollNode.contentWidth = unboundActionsWidth
self.containerNode.layout()
self.scrollNode.layout()
}
func animateIn() {
self.containerNode.layer.animateSpring(from: NSNumber(value: Float(0.2)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.4)
let containerPosition = self.containerNode.layer.position
self.containerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: containerPosition.x, y: containerPosition.y + (self.arrowOnBottom ? 1.0 : -1.0) * self.containerNode.bounds.size.height / 2.0)), to: NSValue(cgPoint: containerPosition), keyPath: "position", duration: 0.4)
self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
if let feedback = self.feedback {
feedback.impact(.light)
}
}
func animateOut(completion: @escaping () -> Void) {
self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
completion()
})
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let event = event {
var eventIsPresses = false
if #available(iOSApplicationExtension 9.0, *) {
eventIsPresses = event.type == .presses
}
if event.type == .touches || eventIsPresses {
if !self.containerNode.frame.contains(point) {
if !self.dismissedByTouchOutside {
self.dismissedByTouchOutside = true
self.dismiss()
}
if self.catchTapsOutside {
return self.view
}
return nil
}
}
}
return super.hitTest(point, with: event)
}
}

View File

@ -0,0 +1,184 @@
import UIKit
public enum DeviceMetrics: CaseIterable {
case iPhone4
case iPhone5
case iPhone6
case iPhone6Plus
case iPhoneX
case iPhoneXSMax
case iPad
case iPadPro10Inch
case iPadPro11Inch
case iPadPro
case iPadPro3rdGen
public static func forScreenSize(_ size: CGSize, hintHasOnScreenNavigation: Bool = false) -> DeviceMetrics? {
let additionalSize = CGSize(width: size.width, height: size.height + 20.0)
for device in DeviceMetrics.allCases {
let width = device.screenSize.width
let height = device.screenSize.height
if ((size.width.isEqual(to: width) && size.height.isEqual(to: height)) || size.height.isEqual(to: width) && size.width.isEqual(to: height)) || ((additionalSize.width.isEqual(to: width) && additionalSize.height.isEqual(to: height)) || additionalSize.height.isEqual(to: width) && additionalSize.width.isEqual(to: height)) {
if hintHasOnScreenNavigation && device.onScreenNavigationHeight(inLandscape: false) == nil {
continue
}
return device
}
}
return nil
}
var screenSize: CGSize {
switch self {
case .iPhone4:
return CGSize(width: 320.0, height: 480.0)
case .iPhone5:
return CGSize(width: 320.0, height: 568.0)
case .iPhone6:
return CGSize(width: 375.0, height: 667.0)
case .iPhone6Plus:
return CGSize(width: 414.0, height: 736.0)
case .iPhoneX:
return CGSize(width: 375.0, height: 812.0)
case .iPhoneXSMax:
return CGSize(width: 414.0, height: 896.0)
case .iPad:
return CGSize(width: 768.0, height: 1024.0)
case .iPadPro10Inch:
return CGSize(width: 834.0, height: 1112.0)
case .iPadPro11Inch:
return CGSize(width: 834.0, height: 1194.0)
case .iPadPro, .iPadPro3rdGen:
return CGSize(width: 1024.0, height: 1366.0)
}
}
func safeAreaInsets(inLandscape: Bool) -> UIEdgeInsets {
switch self {
case .iPhoneX, .iPhoneXSMax:
return inLandscape ? UIEdgeInsets(top: 0.0, left: 44.0, bottom: 0.0, right: 44.0) : UIEdgeInsets(top: 44.0, left: 0.0, bottom: 0.0, right: 0.0)
default:
return UIEdgeInsets.zero
}
}
func onScreenNavigationHeight(inLandscape: Bool) -> CGFloat? {
switch self {
case .iPhoneX, .iPhoneXSMax:
return inLandscape ? 21.0 : 34.0
case .iPadPro3rdGen, .iPadPro11Inch:
return 21.0
default:
return nil
}
}
var statusBarHeight: CGFloat {
switch self {
case .iPhoneX, .iPhoneXSMax:
return 44.0
case .iPadPro11Inch, .iPadPro3rdGen:
return 24.0
default:
return 20.0
}
}
public func standardInputHeight(inLandscape: Bool) -> CGFloat {
if inLandscape {
switch self {
case .iPhone4, .iPhone5:
return 162.0
case .iPhone6, .iPhone6Plus:
return 163.0
case .iPhoneX, .iPhoneXSMax:
return 172.0
case .iPad, .iPadPro10Inch:
return 348.0
case .iPadPro11Inch:
return 368.0
case .iPadPro:
return 421.0
case .iPadPro3rdGen:
return 441.0
}
} else {
switch self {
case .iPhone4, .iPhone5, .iPhone6:
return 216.0
case .iPhone6Plus:
return 227.0
case .iPhoneX:
return 291.0
case .iPhoneXSMax:
return 302.0
case .iPad, .iPadPro10Inch:
return 263.0
case .iPadPro11Inch:
return 283.0
case .iPadPro:
return 328.0
case .iPadPro3rdGen:
return 348.0
}
}
}
func predictiveInputHeight(inLandscape: Bool) -> CGFloat {
if inLandscape {
switch self {
case .iPhone4, .iPhone5, .iPhone6, .iPhone6Plus, .iPhoneX, .iPhoneXSMax:
return 37.0
case .iPad, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen:
return 50.0
}
} else {
switch self {
case .iPhone4, .iPhone5:
return 37.0
case .iPhone6, .iPhone6Plus, .iPhoneX, .iPhoneXSMax:
return 44.0
case .iPad, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen:
return 50.0
}
}
}
public func previewingContentSize(inLandscape: Bool) -> CGSize {
let screenSize = self.screenSize
if inLandscape {
switch self {
case .iPhone5:
return CGSize(width: screenSize.height, height: screenSize.width - 10.0)
case .iPhone6:
return CGSize(width: screenSize.height, height: screenSize.width - 22.0)
case .iPhone6Plus:
return CGSize(width: screenSize.height, height: screenSize.width - 22.0)
case .iPhoneX:
return CGSize(width: screenSize.height, height: screenSize.width + 48.0)
case .iPhoneXSMax:
return CGSize(width: screenSize.height, height: screenSize.width - 30.0)
default:
return CGSize(width: screenSize.height, height: screenSize.width - 10.0)
}
} else {
switch self {
case .iPhone5:
return CGSize(width: screenSize.width, height: screenSize.height - 50.0)
case .iPhone6:
return CGSize(width: screenSize.width, height: screenSize.height - 97.0)
case .iPhone6Plus:
return CGSize(width: screenSize.width, height: screenSize.height - 95.0)
case .iPhoneX:
return CGSize(width: screenSize.width, height: screenSize.height - 154.0)
case .iPhoneXSMax:
return CGSize(width: screenSize.width, height: screenSize.height - 84.0)
default:
return CGSize(width: screenSize.width, height: screenSize.height - 50.0)
}
}
}
}

View File

@ -0,0 +1,34 @@
//
// Display.h
// Display
//
// Created by Peter on 29/07/15.
// Copyright © 2015 Telegram. All rights reserved.
//
#import <UIKit/UIKit.h>
//! Project version number for Display.
FOUNDATION_EXPORT double DisplayVersionNumber;
//! Project version string for Display.
FOUNDATION_EXPORT const unsigned char DisplayVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <Display/PublicHeader.h>
#import <Display/RuntimeUtils.h>
#import <Display/UIViewController+Navigation.h>
#import <Display/UIKitUtils.h>
#import <Display/UINavigationItem+Proxy.h>
#import <Display/UIWindow+OrientationChange.h>
#import <Display/NotificationCenterUtils.h>
#import <Display/NSBag.h>
#import <Display/UIBarButtonItem+Proxy.h>
#import <Display/NavigationControllerProxy.h>
#import <Display/NavigationBarProxy.h>
#import <UIKit/UIGestureRecognizerSubclass.h>
#import <Display/NSWeakReference.h>
#import <Display/FBAnimationPerformanceTracker.h>
#import <Display/CATracingLayer.h>
#import <Display/CASeeThroughTracingLayer.h>
#import <Display/UIMenuItem+Icons.h>

View File

@ -0,0 +1,47 @@
import Foundation
import UIKit
public class DisplayLinkDispatcher: NSObject {
private var displayLink: CADisplayLink!
private var blocksToDispatch: [() -> Void] = []
private let limit: Int
public init(limit: Int = 0) {
self.limit = limit
super.init()
if #available(iOS 10.0, *) {
//self.displayLink.preferredFramesPerSecond = 60
} else {
self.displayLink = CADisplayLink(target: self, selector: #selector(self.run))
self.displayLink.isPaused = true
self.displayLink.add(to: RunLoop.main, forMode: RunLoopMode.commonModes)
}
}
public func dispatch(f: @escaping () -> Void) {
if self.displayLink == nil {
if Thread.isMainThread {
f()
} else {
DispatchQueue.main.async(execute: f)
}
} else {
self.blocksToDispatch.append(f)
self.displayLink.isPaused = false
}
}
@objc func run() {
for _ in 0 ..< (self.limit == 0 ? 1000 : self.limit) {
if self.blocksToDispatch.count == 0 {
self.displayLink.isPaused = true
break
} else {
let f = self.blocksToDispatch.removeFirst()
f()
}
}
}
}

View File

@ -0,0 +1,24 @@
import Foundation
import UIKit
import AsyncDisplayKit
public class EditableTextNode: ASEditableTextNode {
override public var keyboardAppearance: UIKeyboardAppearance {
get {
return super.keyboardAppearance
}
set {
guard newValue != self.keyboardAppearance else {
return
}
let resigning = self.isFirstResponder()
if resigning {
self.resignFirstResponder()
}
super.keyboardAppearance = newValue
if resigning {
self.becomeFirstResponder()
}
}
}
}

View File

@ -0,0 +1,136 @@
#import <Foundation/Foundation.h>
/*
* This is an example provided by Facebook are for non-commercial testing and
* evaluation purposes only.
*
* Facebook reserves all rights not expressly granted.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
*
* FBAnimationPerformanceTracker
* -----------------------------------------------------------------------
*
* This class provides animation performance tracking functionality. It basically
* measures the app's frame rate during an operation, and reports this information.
*
* 1) In Foo's designated initializer, construct a tracker object
*
* 2) Add calls to -start and -stop in appropriate places, e.g. for a ScrollView
*
* - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
* [_apTracker start];
* }
*
* - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
* {
* if (!scrollView.dragging) {
* [_apTracker stop];
* }
* }
*
* - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
* if (!decelerate) {
* [_apTracker stop];
* }
* }
*
* Notes
* -----
* [] The tracker operates by creating a CADisplayLink object to measure the frame rate of the display
* during start/stop interval.
*
* [] Calls to -stop that were not preceded by a matching call to -start have no effect.
*
* [] 2 calls to -start in a row will trash the data accumulated so far and not log anything.
*
*
* Configuration object for the core tracker
*
* ===============================================================================
* I highly recommend for you to use the standard configuration provided
* These are essentially here so that the computation of the metric is transparent
* and you can feel confident in what the numbers mean.
* ===============================================================================
*/
struct FBAnimationPerformanceTrackerConfig
{
// Number of frame drop that defines a "small" drop event. By default, 1.
NSInteger smallDropEventFrameNumber;
// Number of frame drop that defines a "large" drop event. By default, 4.
NSInteger largeDropEventFrameNumber;
// Number of maximum frame drops to which the drop will be trimmed down to. Currently 15.
NSInteger maxFrameDropAccount;
// If YES, will report stack traces
BOOL reportStackTraces;
};
typedef struct FBAnimationPerformanceTrackerConfig FBAnimationPerformanceTrackerConfig;
@protocol FBAnimationPerformanceTrackerDelegate <NSObject>
/**
* Core Metric
*
* You are responsible for the aggregation of these metrics (it being on the client or the server). I recommend to implement both
* to limit the payload you are sending to the server.
*
* The final recommended metric being: - SUM(duration) / SUM(smallDropEvent) aka the number of seconds between one frame drop or more
* - SUM(duration) / SUM(largeDropEvent) aka the number of seconds between four frame drops or more
*
* The first metric will tell you how smooth is your scroll view.
* The second metric will tell you how clowny your scroll view can get.
*
* Every time stop is called, this event will fire reporting the performance.
*
* NOTE on this metric:
* - It has been tested at scale on many Facebook apps.
* - It follows the curves of devices.
* - You will need about 100K calls for the number to converge.
* - It is perfectly correlated to X = Percentage of time spent at 60fps. Number of seconds between one frame drop = 1 / ( 1 - Time spent at 60 fps)
* - We report fraction of drops. 7 frame drop = 1.75 of a large frame drop if a large drop is 4 frame drop.
* This is to preserve the correlation mentionned above.
*/
- (void)reportDurationInMS:(NSInteger)duration smallDropEvent:(double)smallDropEvent largeDropEvent:(double)largeDropEvent;
/**
* Stack traces
*
* Dark magic of the animation tracker. In case of a frame drop, this will return a stack trace.
* This will NOT be reported on the main-thread, but off-main thread to save a few CPU cycles.
*
* The slide is constant value that needs to be reported with the stack for processing.
* This currently only allows for symbolication of your own image.
*
* Future work includes symbolicating all modules. I personnaly find it usually
* good enough to know the name of the module.
*
* The stack will have the following format:
* Foundation:0x123|MyApp:0x234|MyApp:0x345|
*
* The slide will have the following format:
* 0x456
*/
- (void)reportStackTrace:(NSString *)stack withSlide:(NSString *)slide;
@end
@interface FBAnimationPerformanceTracker : NSObject
- (instancetype)initWithConfig:(FBAnimationPerformanceTrackerConfig)config;
+ (FBAnimationPerformanceTrackerConfig)standardConfig;
@property (weak, nonatomic, readwrite) id<FBAnimationPerformanceTrackerDelegate> delegate;
- (void)start;
- (void)stop;
@end

View File

@ -0,0 +1,412 @@
//
// FBAnimationPerformanceTracker.m
// Display
//
// Created by Peter on 3/16/16.
// Copyright © 2016 Telegram. All rights reserved.
//
#import "FBAnimationPerformanceTracker.h"
#import <dlfcn.h>
#import <map>
#import <pthread.h>
#import <QuartzCore/CADisplayLink.h>
#import <mach-o/dyld.h>
#import "execinfo.h"
#include <mach/mach_time.h>
static BOOL _signalSetup;
static pthread_t _mainThread;
static NSThread *_trackerThread;
static std::map<void *, NSString *, std::greater<void *>> _imageNames;
#ifdef __LP64__
typedef mach_header_64 fb_mach_header;
typedef segment_command_64 fb_mach_segment_command;
#define LC_SEGMENT_ARCH LC_SEGMENT_64
#else
typedef mach_header fb_mach_header;
typedef segment_command fb_mach_segment_command;
#define LC_SEGMENT_ARCH LC_SEGMENT
#endif
static volatile BOOL _scrolling;
pthread_mutex_t _scrollingMutex;
pthread_cond_t _scrollingCondVariable;
dispatch_queue_t _symbolicationQueue;
// We record at most 16 frames since I cap the number of frames dropped measured at 15.
// Past 15, something went very wrong (massive contention, priority inversion, rpc call going wrong...) .
// It will only pollute the data to get more.
static const int callstack_max_number = 16;
static int callstack_i;
static bool callstack_dirty;
static int callstack_size[callstack_max_number];
static void *callstacks[callstack_max_number][128];
uint64_t callstack_time_capture;
static void _callstack_signal_handler(int signr, siginfo_t *info, void *secret)
{
// This is run on the main thread every 16 ms or so during scroll.
// Signals are run one by one so there is no risk of concurrency of a signal
// by the same signal.
// The backtrace call is technically signal-safe on Unix-based system
// See: http://www.unix.com/man-page/all/3c/walkcontext/
// WARNING: this is signal handler, no memory allocation is safe.
// Essentially nothing is safe unless specified it is.
callstack_size[callstack_i] = backtrace(callstacks[callstack_i], 128);
callstack_i = (callstack_i + 1) & (callstack_max_number - 1); // & is a cheap modulo (only works for power of 2)
callstack_dirty = true;
}
@interface FBCallstack : NSObject
@property (nonatomic, readonly, assign) int size;
@property (nonatomic, readonly, assign) void **callstack;
- (instancetype)initWithSize:(int)size callstack:(void *)callstack;
@end
@implementation FBCallstack
- (instancetype)initWithSize:(int)size callstack:(void *)callstack
{
if (self = [super init]) {
_size = size;
_callstack = (void **)malloc(size * sizeof(void *));
memcpy(_callstack, callstack, size * sizeof(void *));
}
return self;
}
- (void)dealloc
{
free(_callstack);
}
@end
@implementation FBAnimationPerformanceTracker
{
FBAnimationPerformanceTrackerConfig _config;
BOOL _tracking;
BOOL _firstUpdate;
NSTimeInterval _previousFrameTimestamp;
CADisplayLink *_displayLink;
BOOL _prepared;
// numbers used to track the performance metrics
double _durationTotal;
double _maxFrameTime;
double _smallDrops;
double _largeDrops;
}
- (instancetype)initWithConfig:(FBAnimationPerformanceTrackerConfig)config
{
if (self = [super init]) {
// Stack trace logging is not working well in debug mode
// We don't want the data anyway. So let's bail.
#if defined(DEBUG)
config.reportStackTraces = NO;
#endif
_config = config;
if (config.reportStackTraces) {
[self _setupSignal];
}
}
return self;
}
+ (FBAnimationPerformanceTrackerConfig)standardConfig
{
FBAnimationPerformanceTrackerConfig config = {
.smallDropEventFrameNumber = 1,
.largeDropEventFrameNumber = 4,
.maxFrameDropAccount = 15,
.reportStackTraces = NO
};
return config;
}
+ (void)_trackerLoop
{
while (true) {
// If you are confused by this part,
// Check out https://computing.llnl.gov/tutorials/pthreads/#ConditionVariables
// Lock the mutex
pthread_mutex_lock(&_scrollingMutex);
while (!_scrolling) {
// Unlock the mutex and sleep until the conditional variable is signaled
pthread_cond_wait(&_scrollingCondVariable, &_scrollingMutex);
// The conditional variable was signaled, but we need to check _scrolling
// As nothing guarantees that it is still true
}
// _scrolling is true, go ahead and capture traces for a while.
pthread_mutex_unlock(&_scrollingMutex);
// We are scrolling, yay, capture traces
while (_scrolling) {
usleep(16000);
// Here I use SIGPROF which is a signal supposed to be used for profiling
// I haven't stumbled upon any collision so far.
// There is no guarantee that it won't impact the system in unpredicted ways.
// Use wisely.
pthread_kill(_mainThread, SIGPROF);
}
}
}
- (void)_setupSignal
{
if (!_signalSetup) {
// The signal hook should be setup once and only once
_signalSetup = YES;
// I actually don't know if the main thread can die. If it does, well,
// this is not going to work.
// UPDATE 4/2015: on iOS8, it looks like the main-thread never dies, and this pointer is correct
_mainThread = pthread_self();
callstack_i = 0;
// Setup the signal
struct sigaction sa;
sigfillset(&sa.sa_mask);
sa.sa_flags = SA_SIGINFO;
sa.sa_sigaction = _callstack_signal_handler;
sigaction(SIGPROF, &sa, NULL);
pthread_mutex_init(&_scrollingMutex, NULL);
pthread_cond_init (&_scrollingCondVariable, NULL);
// Setup the signal firing loop
_trackerThread = [[NSThread alloc] initWithTarget:[self class] selector:@selector(_trackerLoop) object:nil];
// We wanna be higher priority than the main thread
// On iOS8 : this will roughly stick us at priority 61, while the main thread oscillates between 20 and 47
_trackerThread.threadPriority = 1.0;
[_trackerThread start];
_symbolicationQueue = dispatch_queue_create("com.facebook.symbolication", DISPATCH_QUEUE_SERIAL);
dispatch_async(_symbolicationQueue, ^(void) {[self _setupSymbolication];});
}
}
- (void)_setupSymbolication
{
// This extract the starting slide of every module in the app
// This is used to know which module an instruction pointer belongs to.
// These operations is NOT thread-safe according to Apple docs
// Do not call this multiple times
int images = _dyld_image_count();
for (int i = 0; i < images; i ++) {
intptr_t imageSlide = _dyld_get_image_vmaddr_slide(i);
// Here we extract the module name from the full path
// Typically it looks something like: /path/to/lib/UIKit
// And I just extract UIKit
NSString *fullName = [NSString stringWithUTF8String:_dyld_get_image_name(i)];
NSRange range = [fullName rangeOfString:@"/" options:NSBackwardsSearch];
NSUInteger startP = (range.location != NSNotFound) ? range.location + 1 : 0;
NSString *imageName = [fullName substringFromIndex:startP];
// This is parsing the mach header in order to extract the slide.
// See https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/MachORuntime/index.html
// For the structure of mach headers
fb_mach_header *header = (fb_mach_header*)_dyld_get_image_header(i);
if (!header) {
continue;
}
const struct load_command *cmd =
reinterpret_cast<const struct load_command *>(header + 1);
for (unsigned int c = 0; cmd && (c < header->ncmds); c++) {
if (cmd->cmd == LC_SEGMENT_ARCH) {
const fb_mach_segment_command *seg =
reinterpret_cast<const fb_mach_segment_command *>(cmd);
if (!strcmp(seg->segname, "__TEXT")) {
_imageNames[(void *)(seg->vmaddr + imageSlide)] = imageName;
break;
}
}
cmd = reinterpret_cast<struct load_command*>((char *)cmd + cmd->cmdsize);
}
}
}
- (void)dealloc
{
if (_prepared) {
[self _tearDownCADisplayLink];
}
}
#pragma mark - Tracking
- (void)start
{
if (!_tracking) {
if ([self prepare]) {
_displayLink.paused = NO;
_tracking = YES;
[self _reset];
if (_config.reportStackTraces) {
pthread_mutex_lock(&_scrollingMutex);
_scrolling = YES;
// Signal the tracker thread to start firing the signals
pthread_cond_signal(&_scrollingCondVariable);
pthread_mutex_unlock(&_scrollingMutex);
}
}
}
}
- (void)stop
{
if (_tracking) {
_tracking = NO;
_displayLink.paused = YES;
if (_durationTotal > 0) {
[_delegate reportDurationInMS:round(1000.0 * _durationTotal) smallDropEvent:_smallDrops largeDropEvent:_largeDrops];
if (_config.reportStackTraces) {
pthread_mutex_lock(&_scrollingMutex);
_scrolling = NO;
pthread_mutex_unlock(&_scrollingMutex);
}
}
}
}
- (BOOL)prepare
{
if (_prepared) {
return YES;
}
[self _setUpCADisplayLink];
_prepared = YES;
return YES;
}
- (void)_setUpCADisplayLink
{
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_update)];
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
_displayLink.paused = YES;
}
- (void)_tearDownCADisplayLink
{
[_displayLink invalidate];
_displayLink = nil;
}
- (void)_reset
{
_firstUpdate = YES;
_previousFrameTimestamp = 0.0;
_durationTotal = 0;
_maxFrameTime = 0;
_largeDrops = 0;
_smallDrops = 0;
}
- (void)_addFrameTime:(NSTimeInterval)actualFrameTime singleFrameTime:(NSTimeInterval)singleFrameTime
{
_maxFrameTime = MAX(actualFrameTime, _maxFrameTime);
NSInteger frameDropped = round(actualFrameTime / singleFrameTime) - 1;
frameDropped = MAX(frameDropped, 0);
// This is to reduce noise. Massive frame drops will just add noise to your data.
frameDropped = MIN(_config.maxFrameDropAccount, frameDropped);
_durationTotal += (frameDropped + 1) * singleFrameTime;
// We account 2 frame drops as 2 small events. This way the metric correlates perfectly with Time at X fps.
_smallDrops += (frameDropped >= _config.smallDropEventFrameNumber) ? ((double) frameDropped) / (double)_config.smallDropEventFrameNumber : 0.0;
_largeDrops += (frameDropped >= _config.largeDropEventFrameNumber) ? ((double) frameDropped) / (double)_config.largeDropEventFrameNumber : 0.0;
if (frameDropped >= 1) {
if (_config.reportStackTraces) {
callstack_dirty = false;
for (int ci = 0; ci <= frameDropped ; ci ++) {
// This is computing the previous indexes
// callstack - 1 - ci takes us back ci frames
// I want a positive number so I add callstack_max_number
// And then just modulo it, with & (callstack_max_number - 1)
int callstackPreviousIndex = ((callstack_i - 1 - ci) + callstack_max_number) & (callstack_max_number - 1);
FBCallstack *callstackCopy = [[FBCallstack alloc] initWithSize:callstack_size[callstackPreviousIndex] callstack:callstacks[callstackPreviousIndex]];
// Check that in between the beginning and the end of the copy the signal did not fire
if (!callstack_dirty) {
// The copy has been made. We are now fine, let's punt the rest off main-thread.
__weak FBAnimationPerformanceTracker *weakSelf = self;
dispatch_async(_symbolicationQueue, ^(void) {
[weakSelf _reportStackTrace:callstackCopy];
});
}
}
}
}
}
- (void)_update
{
if (!_tracking) {
return;
}
if (_firstUpdate) {
_firstUpdate = NO;
_previousFrameTimestamp = _displayLink.timestamp;
return;
}
NSTimeInterval currentTimestamp = _displayLink.timestamp;
NSTimeInterval frameTime = currentTimestamp - _previousFrameTimestamp;
[self _addFrameTime:frameTime singleFrameTime:_displayLink.duration];
_previousFrameTimestamp = currentTimestamp;
}
- (void)_reportStackTrace:(FBCallstack *)callstack
{
static NSString *slide;
static dispatch_once_t slide_predicate;
dispatch_once(&slide_predicate, ^{
slide = [NSString stringWithFormat:@"%p", (void *)_dyld_get_image_header(0)];
});
@autoreleasepool {
NSMutableString *stack = [NSMutableString string];
for (int j = 2; j < callstack.size; j ++) {
void *instructionPointer = callstack.callstack[j];
auto it = _imageNames.lower_bound(instructionPointer);
NSString *imageName = (it != _imageNames.end()) ? it->second : @"???";
[stack appendString:imageName];
[stack appendString:@":"];
[stack appendString:[NSString stringWithFormat:@"%p", instructionPointer]];
[stack appendString:@"|"];
}
[_delegate reportStackTrace:stack withSlide:slide];
}
}
@end

View File

@ -0,0 +1,84 @@
import Foundation
import UIKit
public struct Font {
public static func regular(_ size: CGFloat) -> UIFont {
return UIFont.systemFont(ofSize: size)
}
public static func medium(_ size: CGFloat) -> UIFont {
if #available(iOS 8.2, *) {
return UIFont.systemFont(ofSize: size, weight: UIFont.Weight.medium)
} else {
return CTFontCreateWithName("HelveticaNeue-Medium" as CFString, size, nil)
}
}
public static func semibold(_ size: CGFloat) -> UIFont {
if #available(iOS 8.2, *) {
return UIFont.systemFont(ofSize: size, weight: UIFont.Weight.semibold)
} else {
return CTFontCreateWithName("HelveticaNeue-Medium" as CFString, size, nil)
}
}
public static func bold(_ size: CGFloat) -> UIFont {
if #available(iOS 8.2, *) {
return UIFont.boldSystemFont(ofSize: size)
} else {
return CTFontCreateWithName("HelveticaNeue-Bold" as CFString, size, nil)
}
}
public static func light(_ size: CGFloat) -> UIFont {
if #available(iOS 8.2, *) {
return UIFont.systemFont(ofSize: size, weight: UIFont.Weight.light)
} else {
return CTFontCreateWithName("HelveticaNeue-Light" as CFString, size, nil)
}
}
public static func semiboldItalic(_ size: CGFloat) -> UIFont {
if let descriptor = UIFont.systemFont(ofSize: size).fontDescriptor.withSymbolicTraits([.traitBold, .traitItalic]) {
return UIFont(descriptor: descriptor, size: size)
} else {
return UIFont.italicSystemFont(ofSize: size)
}
}
public static func monospace(_ size: CGFloat) -> UIFont {
return UIFont(name: "Menlo-Regular", size: size - 1.0) ?? UIFont.systemFont(ofSize: size)
}
public static func semiboldMonospace(_ size: CGFloat) -> UIFont {
return UIFont(name: "Menlo-Bold", size: size - 1.0) ?? UIFont.systemFont(ofSize: size)
}
public static func italicMonospace(_ size: CGFloat) -> UIFont {
return UIFont(name: "Menlo-Italic", size: size - 1.0) ?? UIFont.systemFont(ofSize: size)
}
public static func semiboldItalicMonospace(_ size: CGFloat) -> UIFont {
return UIFont(name: "Menlo-BoldItalic", size: size - 1.0) ?? UIFont.systemFont(ofSize: size)
}
public static func italic(_ size: CGFloat) -> UIFont {
return UIFont.italicSystemFont(ofSize: size)
}
}
public extension NSAttributedString {
convenience init(string: String, font: UIFont? = nil, textColor: UIColor = UIColor.black, paragraphAlignment: NSTextAlignment? = nil) {
var attributes: [NSAttributedStringKey: AnyObject] = [:]
if let font = font {
attributes[NSAttributedStringKey.font] = font
}
attributes[NSAttributedStringKey.foregroundColor] = textColor
if let paragraphAlignment = paragraphAlignment {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = paragraphAlignment
attributes[NSAttributedStringKey.paragraphStyle] = paragraphStyle
}
self.init(string: string, attributes: attributes)
}
}

View File

@ -0,0 +1,493 @@
import Foundation
import UIKit
let deviceColorSpace = CGColorSpaceCreateDeviceRGB()
let deviceScale = UIScreen.main.scale
public func generateImagePixel(_ size: CGSize, pixelGenerator: (CGSize, UnsafeMutablePointer<Int8>) -> Void) -> UIImage? {
let scale = deviceScale
let scaledSize = CGSize(width: size.width * scale, height: size.height * scale)
let bytesPerRow = (4 * Int(scaledSize.width) + 15) & (~15)
let length = bytesPerRow * Int(scaledSize.height)
let bytes = malloc(length)!.assumingMemoryBound(to: Int8.self)
guard let provider = CGDataProvider(dataInfo: bytes, data: bytes, size: length, releaseData: { bytes, _, _ in
free(bytes)
})
else {
return nil
}
pixelGenerator(scaledSize, bytes)
let bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue)
guard let image = CGImage(width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: false, intent: .defaultIntent)
else {
return nil
}
return UIImage(cgImage: image, scale: scale, orientation: .up)
}
public func generateImage(_ size: CGSize, contextGenerator: (CGSize, CGContext) -> Void, opaque: Bool = false, scale: CGFloat? = nil) -> UIImage? {
let selectedScale = scale ?? deviceScale
let scaledSize = CGSize(width: size.width * selectedScale, height: size.height * selectedScale)
let bytesPerRow = (4 * Int(scaledSize.width) + 15) & (~15)
let length = bytesPerRow * Int(scaledSize.height)
let bytes = malloc(length)!.assumingMemoryBound(to: Int8.self)
guard let provider = CGDataProvider(dataInfo: bytes, data: bytes, size: length, releaseData: { bytes, _, _ in
free(bytes)
})
else {
return nil
}
let bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | (opaque ? CGImageAlphaInfo.noneSkipFirst.rawValue : CGImageAlphaInfo.premultipliedFirst.rawValue))
guard let context = CGContext(data: bytes, width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo.rawValue) else {
return nil
}
context.scaleBy(x: selectedScale, y: selectedScale)
contextGenerator(size, context)
guard let image = CGImage(width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: false, intent: .defaultIntent)
else {
return nil
}
return UIImage(cgImage: image, scale: selectedScale, orientation: .up)
}
public func generateImage(_ size: CGSize, opaque: Bool = false, scale: CGFloat? = nil, rotatedContext: (CGSize, CGContext) -> Void) -> UIImage? {
let selectedScale = scale ?? deviceScale
let scaledSize = CGSize(width: size.width * selectedScale, height: size.height * selectedScale)
let bytesPerRow = (4 * Int(scaledSize.width) + 15) & (~15)
let length = bytesPerRow * Int(scaledSize.height)
let bytes = malloc(length)!.assumingMemoryBound(to: Int8.self)
guard let provider = CGDataProvider(dataInfo: bytes, data: bytes, size: length, releaseData: { bytes, _, _ in
free(bytes)
})
else {
return nil
}
let bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | (opaque ? CGImageAlphaInfo.noneSkipFirst.rawValue : CGImageAlphaInfo.premultipliedFirst.rawValue))
guard let context = CGContext(data: bytes, width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo.rawValue) else {
return nil
}
context.scaleBy(x: selectedScale, y: selectedScale)
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
rotatedContext(size, context)
guard let image = CGImage(width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: false, intent: .defaultIntent)
else {
return nil
}
return UIImage(cgImage: image, scale: selectedScale, orientation: .up)
}
public func generateFilledCircleImage(diameter: CGFloat, color: UIColor?, strokeColor: UIColor? = nil, strokeWidth: CGFloat? = nil, backgroundColor: UIColor? = nil) -> UIImage? {
return generateImage(CGSize(width: diameter, height: diameter), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
if let backgroundColor = backgroundColor {
context.setFillColor(backgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
}
if let strokeColor = strokeColor, let strokeWidth = strokeWidth {
context.setFillColor(strokeColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
if let color = color {
context.setFillColor(color.cgColor)
} else {
context.setFillColor(UIColor.clear.cgColor)
context.setBlendMode(.copy)
}
context.fillEllipse(in: CGRect(origin: CGPoint(x: strokeWidth, y: strokeWidth), size: CGSize(width: size.width - strokeWidth * 2.0, height: size.height - strokeWidth * 2.0)))
} else {
if let color = color {
context.setFillColor(color.cgColor)
} else {
context.setFillColor(UIColor.clear.cgColor)
context.setBlendMode(.copy)
}
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
}
})
}
public func generateCircleImage(diameter: CGFloat, lineWidth: CGFloat, color: UIColor?, strokeColor: UIColor? = nil, strokeWidth: CGFloat? = nil, backgroundColor: UIColor? = nil) -> UIImage? {
return generateImage(CGSize(width: diameter, height: diameter), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
if let backgroundColor = backgroundColor {
context.setFillColor(backgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
}
if let color = color {
context.setStrokeColor(color.cgColor)
} else {
context.setStrokeColor(UIColor.clear.cgColor)
context.setBlendMode(.copy)
}
context.setLineWidth(lineWidth)
context.strokeEllipse(in: CGRect(origin: CGPoint(x: lineWidth / 2.0, y: lineWidth / 2.0), size: CGSize(width: size.width - lineWidth, height: size.height - lineWidth)))
})
}
public func generateStretchableFilledCircleImage(radius: CGFloat, color: UIColor?, backgroundColor: UIColor? = nil) -> UIImage? {
let intRadius = Int(radius)
let cap = intRadius == 1 ? 2 : intRadius
return generateFilledCircleImage(diameter: radius * 2.0, color: color, backgroundColor: backgroundColor)?.stretchableImage(withLeftCapWidth: cap, topCapHeight: cap)
}
public func generateStretchableFilledCircleImage(diameter: CGFloat, color: UIColor?, strokeColor: UIColor? = nil, strokeWidth: CGFloat? = nil, backgroundColor: UIColor? = nil) -> UIImage? {
let intRadius = Int(diameter / 2.0)
let intDiameter = Int(diameter)
let cap: Int
if intDiameter == 3 {
cap = 1
} else if intRadius == 1 {
cap = 2
} else {
cap = intRadius
}
return generateFilledCircleImage(diameter: diameter, color: color, strokeColor: strokeColor, strokeWidth: strokeWidth, backgroundColor: backgroundColor)?.stretchableImage(withLeftCapWidth: cap, topCapHeight: cap)
}
public func generateVerticallyStretchableFilledCircleImage(radius: CGFloat, color: UIColor?, backgroundColor: UIColor? = nil) -> UIImage? {
return generateImage(CGSize(width: radius * 2.0, height: radius * 2.0 + radius), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
if let backgroundColor = backgroundColor {
context.setFillColor(backgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
}
if let color = color {
context.setFillColor(color.cgColor)
} else {
context.setFillColor(UIColor.clear.cgColor)
context.setBlendMode(.copy)
}
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: radius + radius, height: radius + radius)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: radius), size: CGSize(width: radius + radius, height: radius + radius)))
})?.stretchableImage(withLeftCapWidth: Int(radius), topCapHeight: Int(radius))
}
public func generateTintedImage(image: UIImage?, color: UIColor, backgroundColor: UIColor? = nil) -> UIImage? {
guard let image = image else {
return nil
}
let imageSize = image.size
UIGraphicsBeginImageContextWithOptions(imageSize, backgroundColor != nil, image.scale)
if let context = UIGraphicsGetCurrentContext() {
if let backgroundColor = backgroundColor {
context.setFillColor(backgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: imageSize))
}
let imageRect = CGRect(origin: CGPoint(), size: imageSize)
context.saveGState()
context.translateBy(x: imageRect.midX, y: imageRect.midY)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -imageRect.midX, y: -imageRect.midY)
context.clip(to: imageRect, mask: image.cgImage!)
context.setFillColor(color.cgColor)
context.fill(imageRect)
context.restoreGState()
}
let tintedImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return tintedImage
}
public func generateScaledImage(image: UIImage?, size: CGSize, opaque: Bool = true, scale: CGFloat? = nil) -> UIImage? {
guard let image = image else {
return nil
}
return generateImage(size, contextGenerator: { size, context in
if !opaque {
context.clear(CGRect(origin: CGPoint(), size: size))
}
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size))
}, opaque: opaque, scale: scale)
}
private func generateSingleColorImage(size: CGSize, color: UIColor) -> UIImage? {
return generateImage(size, contextGenerator: { size, context in
context.setFillColor(color.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
})
}
public enum DrawingContextBltMode {
case Alpha
}
public class DrawingContext {
public let size: CGSize
public let scale: CGFloat
private let scaledSize: CGSize
public let bytesPerRow: Int
private let bitmapInfo: CGBitmapInfo
public let length: Int
public let bytes: UnsafeMutableRawPointer
let provider: CGDataProvider?
private var _context: CGContext?
public func withContext(_ f: (CGContext) -> ()) {
if self._context == nil {
if let c = CGContext(data: bytes, width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: self.bitmapInfo.rawValue) {
c.scaleBy(x: scale, y: scale)
self._context = c
}
}
if let _context = self._context {
_context.translateBy(x: self.size.width / 2.0, y: self.size.height / 2.0)
_context.scaleBy(x: 1.0, y: -1.0)
_context.translateBy(x: -self.size.width / 2.0, y: -self.size.height / 2.0)
f(_context)
_context.translateBy(x: self.size.width / 2.0, y: self.size.height / 2.0)
_context.scaleBy(x: 1.0, y: -1.0)
_context.translateBy(x: -self.size.width / 2.0, y: -self.size.height / 2.0)
}
}
public func withFlippedContext(_ f: (CGContext) -> ()) {
if self._context == nil {
if let c = CGContext(data: bytes, width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: self.bitmapInfo.rawValue) {
c.scaleBy(x: scale, y: scale)
self._context = c
}
}
if let _context = self._context {
f(_context)
}
}
public init(size: CGSize, scale: CGFloat = 0.0, clear: Bool = false) {
let actualScale: CGFloat
if scale.isZero {
actualScale = deviceScale
} else {
actualScale = scale
}
self.size = size
self.scale = actualScale
self.scaledSize = CGSize(width: size.width * actualScale, height: size.height * actualScale)
self.bytesPerRow = (4 * Int(scaledSize.width) + 15) & (~15)
self.length = bytesPerRow * Int(scaledSize.height)
self.bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue)
self.bytes = malloc(length)!
if clear {
memset(self.bytes, 0, self.length)
}
self.provider = CGDataProvider(dataInfo: bytes, data: bytes, size: length, releaseData: { bytes, _, _ in
free(bytes)
})
assert(self.bytesPerRow % 16 == 0)
assert(Int64(Int(bitPattern: self.bytes)) % 16 == 0)
}
public func generateImage() -> UIImage? {
if self.scaledSize.width.isZero || self.scaledSize.height.isZero {
return nil
}
if let image = CGImage(width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo, provider: provider!, decode: nil, shouldInterpolate: false, intent: .defaultIntent) {
return UIImage(cgImage: image, scale: scale, orientation: .up)
} else {
return nil
}
}
public func colorAt(_ point: CGPoint) -> UIColor {
let x = Int(point.x * self.scale)
let y = Int(point.y * self.scale)
if x >= 0 && x < Int(self.scaledSize.width) && y >= 0 && y < Int(self.scaledSize.height) {
let srcLine = self.bytes.advanced(by: y * self.bytesPerRow).assumingMemoryBound(to: UInt32.self)
let pixel = srcLine + x
let colorValue = pixel.pointee
return UIColor(rgb: UInt32(colorValue))
} else {
return UIColor.clear
}
}
public func blt(_ other: DrawingContext, at: CGPoint, mode: DrawingContextBltMode = .Alpha) {
if abs(other.scale - self.scale) < CGFloat.ulpOfOne {
let srcX = 0
var srcY = 0
let dstX = Int(at.x * self.scale)
var dstY = Int(at.y * self.scale)
if dstX < 0 || dstY < 0 {
return
}
let width = min(Int(self.size.width * self.scale) - dstX, Int(other.size.width * self.scale))
let height = min(Int(self.size.height * self.scale) - dstY, Int(other.size.height * self.scale))
let maxDstX = dstX + width
let maxDstY = dstY + height
switch mode {
case .Alpha:
while dstY < maxDstY {
let srcLine = other.bytes.advanced(by: max(0, srcY) * other.bytesPerRow).assumingMemoryBound(to: UInt32.self)
let dstLine = self.bytes.advanced(by: max(0, dstY) * self.bytesPerRow).assumingMemoryBound(to: UInt32.self)
var dx = dstX
var sx = srcX
while dx < maxDstX {
let srcPixel = srcLine + sx
let dstPixel = dstLine + dx
let baseColor = dstPixel.pointee
let baseAlpha = (baseColor >> 24) & 0xff
let baseR = (baseColor >> 16) & 0xff
let baseG = (baseColor >> 8) & 0xff
let baseB = baseColor & 0xff
let alpha = min(baseAlpha, srcPixel.pointee >> 24)
let r = (baseR * alpha) / 255
let g = (baseG * alpha) / 255
let b = (baseB * alpha) / 255
dstPixel.pointee = (alpha << 24) | (r << 16) | (g << 8) | b
dx += 1
sx += 1
}
dstY += 1
srcY += 1
}
}
}
}
}
public enum ParsingError: Error {
case Generic
}
public func readCGFloat(_ index: inout UnsafePointer<UInt8>, end: UnsafePointer<UInt8>, separator: UInt8) throws -> CGFloat {
let begin = index
var seenPoint = false
while index <= end {
let c = index.pointee
index = index.successor()
if c == 46 { // .
if seenPoint {
throw ParsingError.Generic
} else {
seenPoint = true
}
} else if c == separator {
break
} else if !((c >= 48 && c <= 57) || c == 45 || c == 101 || c == 69) {
throw ParsingError.Generic
}
}
if index == begin {
throw ParsingError.Generic
}
if let value = NSString(bytes: UnsafeRawPointer(begin), length: index - begin, encoding: String.Encoding.utf8.rawValue)?.floatValue {
return CGFloat(value)
} else {
throw ParsingError.Generic
}
}
public func drawSvgPath(_ context: CGContext, path: StaticString, strokeOnMove: Bool = false) throws {
var index: UnsafePointer<UInt8> = path.utf8Start
let end = path.utf8Start.advanced(by: path.utf8CodeUnitCount)
while index < end {
let c = index.pointee
index = index.successor()
if c == 77 { // M
let x = try readCGFloat(&index, end: end, separator: 44)
let y = try readCGFloat(&index, end: end, separator: 32)
//print("Move to \(x), \(y)")
context.move(to: CGPoint(x: x, y: y))
} else if c == 76 { // L
let x = try readCGFloat(&index, end: end, separator: 44)
let y = try readCGFloat(&index, end: end, separator: 32)
//print("Line to \(x), \(y)")
context.addLine(to: CGPoint(x: x, y: y))
if strokeOnMove {
context.strokePath()
context.move(to: CGPoint(x: x, y: y))
}
} else if c == 67 { // C
let x1 = try readCGFloat(&index, end: end, separator: 44)
let y1 = try readCGFloat(&index, end: end, separator: 32)
let x2 = try readCGFloat(&index, end: end, separator: 44)
let y2 = try readCGFloat(&index, end: end, separator: 32)
let x = try readCGFloat(&index, end: end, separator: 44)
let y = try readCGFloat(&index, end: end, separator: 32)
context.addCurve(to: CGPoint(x: x, y: y), control1: CGPoint(x: x1, y: y1), control2: CGPoint(x: x2, y: y2))
//print("Line to \(x), \(y)")
if strokeOnMove {
context.strokePath()
context.move(to: CGPoint(x: x, y: y))
}
} else if c == 90 { // Z
if index != end && index.pointee != 32 {
throw ParsingError.Generic
}
//CGContextClosePath(context)
context.fillPath()
//CGContextBeginPath(context)
//print("Close")
} else if c == 83 { // S
if index != end && index.pointee != 32 {
throw ParsingError.Generic
}
//CGContextClosePath(context)
context.strokePath()
//CGContextBeginPath(context)
//print("Close")
} else if c == 32 { // space
continue
} else {
throw ParsingError.Generic
}
}
}

View File

@ -0,0 +1,189 @@
import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
private func isViewVisibleInHierarchy(_ view: UIView, _ initial: Bool = true) -> Bool {
guard let window = view.window else {
return false
}
if view.isHidden || view.alpha == 0.0 {
return false
}
if view.superview === window {
return true
} else if let superview = view.superview {
if initial && view.frame.minY >= superview.frame.height {
return false
} else {
return isViewVisibleInHierarchy(superview, false)
}
} else {
return false
}
}
final class GlobalOverlayPresentationContext {
private let statusBarHost: StatusBarHost?
private var controllers: [ContainableController] = []
private var presentationDisposables = DisposableSet()
private var layout: ContainerViewLayout?
private var ready: Bool {
return self.currentPresentationView() != nil && self.layout != nil
}
init(statusBarHost: StatusBarHost?) {
self.statusBarHost = statusBarHost
}
private func currentPresentationView() -> UIView? {
if let statusBarHost = self.statusBarHost {
if let keyboardWindow = statusBarHost.keyboardWindow, let keyboardView = statusBarHost.keyboardView, !keyboardView.frame.height.isZero, isViewVisibleInHierarchy(keyboardView) {
return keyboardWindow
} else {
return statusBarHost.statusBarWindow
}
}
return nil
}
func present(_ controller: ContainableController) {
let controllerReady = controller.ready.get()
|> filter({ $0 })
|> take(1)
|> deliverOnMainQueue
|> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(true))
if let _ = self.currentPresentationView(), let initialLayout = self.layout {
controller.view.frame = CGRect(origin: CGPoint(), size: initialLayout.size)
controller.containerLayoutUpdated(initialLayout, transition: .immediate)
self.presentationDisposables.add(controllerReady.start(next: { [weak self] _ in
if let strongSelf = self {
if strongSelf.controllers.contains(where: { $0 === controller }) {
return
}
strongSelf.controllers.append(controller)
if let view = strongSelf.currentPresentationView(), let layout = strongSelf.layout {
(controller as? UIViewController)?.navigation_setDismiss({ [weak controller] in
if let strongSelf = self, let controller = controller {
strongSelf.dismiss(controller)
}
}, rootController: nil)
(controller as? UIViewController)?.setIgnoreAppearanceMethodInvocations(true)
if layout != initialLayout {
controller.view.frame = CGRect(origin: CGPoint(), size: layout.size)
view.addSubview(controller.view)
controller.containerLayoutUpdated(layout, transition: .immediate)
} else {
view.addSubview(controller.view)
}
(controller as? UIViewController)?.setIgnoreAppearanceMethodInvocations(false)
view.layer.invalidateUpTheTree()
controller.viewWillAppear(false)
controller.viewDidAppear(false)
}
}
}))
} else {
self.controllers.append(controller)
}
}
deinit {
self.presentationDisposables.dispose()
}
private func dismiss(_ controller: ContainableController) {
if let index = self.controllers.index(where: { $0 === controller }) {
self.controllers.remove(at: index)
controller.viewWillDisappear(false)
controller.view.removeFromSuperview()
controller.viewDidDisappear(false)
}
}
public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
let wasReady = self.ready
self.layout = layout
if wasReady != self.ready {
self.readyChanged(wasReady: wasReady)
} else if self.ready {
for controller in self.controllers {
controller.containerLayoutUpdated(layout, transition: transition)
}
}
}
private func readyChanged(wasReady: Bool) {
if !wasReady {
self.addViews()
} else {
self.removeViews()
}
}
private func addViews() {
if let view = self.currentPresentationView(), let layout = self.layout {
for controller in self.controllers {
controller.viewWillAppear(false)
view.addSubview(controller.view)
controller.view.frame = CGRect(origin: CGPoint(), size: layout.size)
controller.containerLayoutUpdated(layout, transition: .immediate)
controller.viewDidAppear(false)
}
}
}
private func removeViews() {
for controller in self.controllers {
controller.viewWillDisappear(false)
controller.view.removeFromSuperview()
controller.viewDidDisappear(false)
}
}
func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
for controller in self.controllers.reversed() {
if controller.isViewLoaded {
if let result = controller.view.hitTest(point, with: event) {
return result
}
}
}
return nil
}
func updateToInterfaceOrientation(_ orientation: UIInterfaceOrientation) {
if self.ready {
for controller in self.controllers {
controller.updateToInterfaceOrientation(orientation)
}
}
}
func combinedSupportedOrientations(currentOrientationToLock: UIInterfaceOrientationMask) -> ViewControllerSupportedOrientations {
var mask = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .all)
for controller in self.controllers {
mask = mask.intersection(controller.combinedSupportedOrientations(currentOrientationToLock: currentOrientationToLock))
}
return mask
}
func combinedDeferScreenEdgeGestures() -> UIRectEdge {
var edges: UIRectEdge = []
for controller in self.controllers {
edges = edges.union(controller.deferScreenEdgeGestures)
}
return edges
}
}

View File

@ -0,0 +1,34 @@
import Foundation
import UIKit
import AsyncDisplayKit
public protocol GridSection {
var height: CGFloat { get }
var hashValue: Int { get }
func isEqual(to: GridSection) -> Bool
func node() -> ASDisplayNode
}
public protocol GridItem {
var section: GridSection? { get }
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode
func update(node: GridItemNode)
var aspectRatio: CGFloat { get }
var fillsRowWithHeight: CGFloat? { get }
var fillsRowWithDynamicHeight: ((CGFloat) -> CGFloat)? { get }
}
public extension GridItem {
var aspectRatio: CGFloat {
return 1.0
}
var fillsRowWithHeight: CGFloat? {
return nil
}
var fillsRowWithDynamicHeight: ((CGFloat) -> CGFloat)? {
return nil
}
}

View File

@ -0,0 +1,21 @@
import Foundation
import UIKit
import AsyncDisplayKit
open class GridItemNode: ASDisplayNode {
open var isVisibleInGrid = false
open var isGridScrolling = false
final var cachedFrame: CGRect = CGRect()
override open var frame: CGRect {
get {
return self.cachedFrame
} set(value) {
self.cachedFrame = value
super.frame = value
}
}
open func updateLayout(item: GridItem, size: CGSize, isVisible: Bool, synchronousLoads: Bool) {
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,53 @@
import UIKit
import AsyncDisplayKit
private class GridNodeScrollerLayer: CALayer {
override func setNeedsDisplay() {
}
}
private class GridNodeScrollerView: UIScrollView {
override class var layerClass: AnyClass {
return GridNodeScrollerLayer.self
}
override init(frame: CGRect) {
super.init(frame: frame)
if #available(iOSApplicationExtension 11.0, *) {
self.contentInsetAdjustmentBehavior = .never
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
@objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
}
open class GridNodeScroller: ASDisplayNode, UIGestureRecognizerDelegate {
public var scrollView: UIScrollView {
return self.view as! UIScrollView
}
override init() {
super.init()
self.setViewBlock({
return GridNodeScrollerView(frame: CGRect())
})
self.scrollView.scrollsToTop = false
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,132 @@
import Foundation
import UIKit
public enum ImpactHapticFeedbackStyle: Hashable {
case light
case medium
case heavy
}
@available(iOSApplicationExtension 10.0, iOS 10.0, *)
private final class HapticFeedbackImpl {
private lazy var impactGenerator: [ImpactHapticFeedbackStyle : UIImpactFeedbackGenerator] = {
[.light: UIImpactFeedbackGenerator(style: .light),
.medium: UIImpactFeedbackGenerator(style: .medium),
.heavy: UIImpactFeedbackGenerator(style: .heavy)] }()
private lazy var selectionGenerator = { UISelectionFeedbackGenerator() }()
private lazy var notificationGenerator = { UINotificationFeedbackGenerator() }()
func prepareTap() {
self.selectionGenerator.prepare()
}
func tap() {
self.selectionGenerator.selectionChanged()
}
func prepareImpact(_ style: ImpactHapticFeedbackStyle) {
self.impactGenerator[style]?.prepare()
}
func impact(_ style: ImpactHapticFeedbackStyle) {
self.impactGenerator[style]?.impactOccurred()
}
func success() {
self.notificationGenerator.notificationOccurred(.success)
}
func prepareError() {
self.notificationGenerator.prepare()
}
func error() {
self.notificationGenerator.notificationOccurred(.error)
}
@objc dynamic func f() {
}
}
public final class HapticFeedback {
private var impl: AnyObject?
public init() {
}
deinit {
let impl = self.impl
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0, execute: {
if #available(iOSApplicationExtension 10.0, *) {
if let impl = impl as? HapticFeedbackImpl {
impl.f()
}
}
})
}
@available(iOSApplicationExtension 10.0, iOS 10.0, *)
private func withImpl(_ f: (HapticFeedbackImpl) -> Void) {
if self.impl == nil {
self.impl = HapticFeedbackImpl()
}
f(self.impl as! HapticFeedbackImpl)
}
public func prepareTap() {
if #available(iOSApplicationExtension 10.0, *) {
self.withImpl { impl in
impl.prepareTap()
}
}
}
public func tap() {
if #available(iOSApplicationExtension 10.0, *) {
self.withImpl { impl in
impl.tap()
}
}
}
public func prepareImpact(_ style: ImpactHapticFeedbackStyle = .medium) {
if #available(iOSApplicationExtension 10.0, *) {
self.withImpl { impl in
impl.prepareImpact(style)
}
}
}
public func impact(_ style: ImpactHapticFeedbackStyle = .medium) {
if #available(iOSApplicationExtension 10.0, *) {
self.withImpl { impl in
impl.impact(style)
}
}
}
public func success() {
if #available(iOSApplicationExtension 10.0, *) {
self.withImpl { impl in
impl.success()
}
}
}
public func prepareError() {
if #available(iOSApplicationExtension 10.0, *) {
self.withImpl { impl in
impl.prepareError()
}
}
}
public func error() {
if #available(iOSApplicationExtension 10.0, *) {
self.withImpl { impl in
impl.error()
}
}
}
}

View File

@ -0,0 +1,48 @@
import UIKit
open class HighlightTrackingButton: UIButton {
private var internalHighlighted = false
public var internalHighligthedChanged: (Bool) -> Void = { _ in }
public var highligthedChanged: (Bool) -> Void = { _ in }
open override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
if !self.internalHighlighted {
self.internalHighlighted = true
self.highligthedChanged(true)
self.internalHighligthedChanged(true)
}
return super.beginTracking(touch, with: event)
}
open override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
if self.internalHighlighted {
self.internalHighlighted = false
self.highligthedChanged(false)
self.internalHighligthedChanged(false)
}
super.endTracking(touch, with: event)
}
open override func cancelTracking(with event: UIEvent?) {
if self.internalHighlighted {
self.internalHighlighted = false
self.highligthedChanged(false)
self.internalHighligthedChanged(false)
}
super.cancelTracking(with: event)
}
open override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
if self.internalHighlighted {
self.internalHighlighted = false
self.highligthedChanged(false)
self.internalHighligthedChanged(false)
}
super.touchesCancelled(touches, with: event)
}
}

View File

@ -0,0 +1,87 @@
import Foundation
import UIKit
import AsyncDisplayKit
open class HighlightableButton: HighlightTrackingButton {
override public init(frame: CGRect) {
super.init(frame: frame)
self.adjustsImageWhenHighlighted = false
self.adjustsImageWhenDisabled = false
self.internalHighligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.layer.removeAnimation(forKey: "opacity")
strongSelf.alpha = 0.4
} else {
strongSelf.alpha = 1.0
strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
open class HighlightTrackingButtonNode: ASButtonNode {
private var internalHighlighted = false
public var highligthedChanged: (Bool) -> Void = { _ in }
open override func beginTracking(with touch: UITouch, with event: UIEvent?) -> Bool {
if !self.internalHighlighted {
self.internalHighlighted = true
self.highligthedChanged(true)
}
return super.beginTracking(with: touch, with: event)
}
open override func endTracking(with touch: UITouch?, with event: UIEvent?) {
if self.internalHighlighted {
self.internalHighlighted = false
self.highligthedChanged(false)
}
super.endTracking(with: touch, with: event)
}
open override func cancelTracking(with event: UIEvent?) {
if self.internalHighlighted {
self.internalHighlighted = false
self.highligthedChanged(false)
}
super.cancelTracking(with: event)
}
open override func touchesCancelled(_ touches: Set<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() {
super.init()
self.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.layer.removeAnimation(forKey: "opacity")
strongSelf.alpha = 0.4
} else {
strongSelf.alpha = 1.0
strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
}
}

View File

@ -0,0 +1,135 @@
import Foundation
import UIKit
public struct ImmediateTextNodeLayoutInfo {
public let size: CGSize
public let truncated: Bool
}
public class ImmediateTextNode: TextNode {
public var attributedText: NSAttributedString?
public var textAlignment: NSTextAlignment = .natural
public var truncationType: CTLineTruncationType = .end
public var maximumNumberOfLines: Int = 1
public var lineSpacing: CGFloat = 0.0
public var insets: UIEdgeInsets = UIEdgeInsets()
private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer?
private var linkHighlightingNode: LinkHighlightingNode?
public var linkHighlightColor: UIColor?
public var trailingLineWidth: CGFloat?
public var highlightAttributeAction: (([NSAttributedStringKey: Any]) -> NSAttributedStringKey?)? {
didSet {
if self.isNodeLoaded {
self.updateInteractiveActions()
}
}
}
public var tapAttributeAction: (([NSAttributedStringKey: Any]) -> Void)?
public var longTapAttributeAction: (([NSAttributedStringKey: Any]) -> Void)?
public func updateLayout(_ constrainedSize: CGSize) -> CGSize {
let makeLayout = TextNode.asyncLayout(self)
let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, lineSpacing: self.lineSpacing, cutout: nil, insets: self.insets))
let _ = apply()
if layout.numberOfLines > 1 {
self.trailingLineWidth = layout.trailingLineWidth
} else {
self.trailingLineWidth = nil
}
return layout.size
}
public func updateLayoutInfo(_ constrainedSize: CGSize) -> ImmediateTextNodeLayoutInfo {
let makeLayout = TextNode.asyncLayout(self)
let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, lineSpacing: self.lineSpacing, cutout: nil, insets: self.insets))
let _ = apply()
return ImmediateTextNodeLayoutInfo(size: layout.size, truncated: layout.truncated)
}
override public func didLoad() {
super.didLoad()
self.updateInteractiveActions()
}
private func updateInteractiveActions() {
if self.highlightAttributeAction != nil {
if self.tapRecognizer == nil {
let tapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapAction(_:)))
tapRecognizer.highlight = { [weak self] point in
if let strongSelf = self {
var rects: [CGRect]?
if let point = point {
if let (index, attributes) = strongSelf.attributesAtPoint(CGPoint(x: point.x, y: point.y)) {
if let selectedAttribute = strongSelf.highlightAttributeAction?(attributes) {
let initialRects = strongSelf.lineAndAttributeRects(name: selectedAttribute.rawValue, at: index)
if let initialRects = initialRects, case .center = strongSelf.textAlignment {
var mappedRects: [CGRect] = []
for i in 0 ..< initialRects.count {
let lineRect = initialRects[i].0
var itemRect = initialRects[i].1
itemRect.origin.x = floor((strongSelf.bounds.size.width - lineRect.width) / 2.0) + itemRect.origin.x
mappedRects.append(itemRect)
}
rects = mappedRects
} else {
rects = strongSelf.attributeRects(name: selectedAttribute.rawValue, at: index)
}
}
}
}
if let rects = rects {
let linkHighlightingNode: LinkHighlightingNode
if let current = strongSelf.linkHighlightingNode {
linkHighlightingNode = current
} else {
linkHighlightingNode = LinkHighlightingNode(color: strongSelf.linkHighlightColor ?? .clear)
strongSelf.linkHighlightingNode = linkHighlightingNode
strongSelf.addSubnode(linkHighlightingNode)
}
linkHighlightingNode.frame = strongSelf.bounds
linkHighlightingNode.updateRects(rects.map { $0.offsetBy(dx: 0.0, dy: 0.0) })
} else if let linkHighlightingNode = strongSelf.linkHighlightingNode {
strongSelf.linkHighlightingNode = nil
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
linkHighlightingNode?.removeFromSupernode()
})
}
}
}
self.view.addGestureRecognizer(tapRecognizer)
}
} else if let tapRecognizer = self.tapRecognizer {
self.tapRecognizer = nil
self.view.removeGestureRecognizer(tapRecognizer)
}
}
@objc private func tapAction(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
if let (_, attributes) = self.attributesAtPoint(CGPoint(x: location.x, y: location.y)) {
self.tapAttributeAction?(attributes)
}
case .longTap:
if let (_, attributes) = self.attributesAtPoint(CGPoint(x: location.x, y: location.y)) {
self.longTapAttributeAction?(attributes)
}
default:
break
}
}
default:
break
}
}
}

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>

View File

@ -0,0 +1,83 @@
import Foundation
import UIKit
private func hasHorizontalGestures(_ view: UIView, point: CGPoint?) -> Bool {
if view.disablesInteractiveTransitionGestureRecognizer {
return true
}
if let point = point, let test = view.interactiveTransitionGestureRecognizerTest, test(point) {
return true
}
if let view = view as? ListViewBackingView {
let transform = view.transform
let angle: Double = Double(atan2f(Float(transform.b), Float(transform.a)))
let term1: Double = abs(angle - Double.pi / 2.0)
let term2: Double = abs(angle + Double.pi / 2.0)
let term3: Double = abs(angle - Double.pi * 3.0 / 2.0)
if term1 < 0.001 || term2 < 0.001 || term3 < 0.001 {
return true
}
}
if let superview = view.superview {
return hasHorizontalGestures(superview, point: point != nil ? view.convert(point!, to: superview) : nil)
} else {
return false
}
}
class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
var validatedGesture = false
var firstLocation: CGPoint = CGPoint()
override init(target: Any?, action: Selector?) {
super.init(target: target, action: action)
self.maximumNumberOfTouches = 1
}
override func reset() {
super.reset()
validatedGesture = false
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
let touch = touches.first!
self.firstLocation = touch.location(in: self.view)
if let target = self.view?.hitTest(self.firstLocation, with: event) {
if hasHorizontalGestures(target, point: self.view?.convert(self.firstLocation, to: target)) {
self.state = .cancelled
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
let location = touches.first!.location(in: self.view)
let translation = CGPoint(x: location.x - firstLocation.x, y: location.y - firstLocation.y)
let absTranslationX: CGFloat = abs(translation.x)
let absTranslationY: CGFloat = abs(translation.y)
if !validatedGesture {
if self.firstLocation.x < 16.0 {
validatedGesture = true
} else if translation.x < 0.0 {
self.state = .failed
} else if absTranslationY > 2.0 && absTranslationY > absTranslationX * 2.0 {
self.state = .failed
} else if absTranslationX > 2.0 && absTranslationY * 2.0 < absTranslationX {
validatedGesture = true
}
}
if validatedGesture {
super.touchesMoved(touches, with: event)
}
}
}

View File

@ -0,0 +1,43 @@
import UIKit
public struct KeyShortcut: Hashable {
let title: String
let input: String
let modifiers: UIKeyModifierFlags
let action: () -> Void
public init(title: String = "", input: String = "", modifiers: UIKeyModifierFlags = [], action: @escaping () -> Void = {}) {
self.title = title
self.input = input
self.modifiers = modifiers
self.action = action
}
public var hashValue: Int {
return input.hashValue ^ modifiers.hashValue
}
public static func ==(lhs: KeyShortcut, rhs: KeyShortcut) -> Bool {
return lhs.hashValue == rhs.hashValue
}
}
extension UIKeyModifierFlags: Hashable {
public var hashValue: Int {
return self.rawValue
}
}
extension KeyShortcut {
var uiKeyCommand: UIKeyCommand {
if #available(iOSApplicationExtension 9.0, *), !self.title.isEmpty {
return UIKeyCommand(input: self.input, modifierFlags: self.modifiers, action: #selector(KeyShortcutsController.handleKeyCommand(_:)), discoverabilityTitle: self.title)
} else {
return UIKeyCommand(input: self.input, modifierFlags: self.modifiers, action: #selector(KeyShortcutsController.handleKeyCommand(_:)))
}
}
func isEqual(to command: UIKeyCommand) -> Bool {
return self.input == command.input && self.modifiers == command.modifierFlags
}
}

View File

@ -0,0 +1,84 @@
import UIKit
public protocol KeyShortcutResponder {
var keyShortcuts: [KeyShortcut] { get };
}
public class KeyShortcutsController: UIResponder {
private var effectiveShortcuts: [KeyShortcut]?
private var viewControllerEnumerator: ((ContainableController) -> Bool) -> Void
public static var isAvailable: Bool {
if #available(iOSApplicationExtension 8.0, *), UIDevice.current.userInterfaceIdiom == .pad {
return true
} else {
return false
}
}
public init(enumerator: @escaping ((ContainableController) -> Bool) -> Void) {
self.viewControllerEnumerator = enumerator
super.init()
}
public override var keyCommands: [UIKeyCommand]? {
var convertedCommands: [UIKeyCommand] = []
var shortcuts: [KeyShortcut] = []
self.viewControllerEnumerator({ viewController -> Bool in
guard let viewController = viewController as? KeyShortcutResponder else {
return true
}
shortcuts.removeAll(where: { viewController.keyShortcuts.contains($0) })
shortcuts.append(contentsOf: viewController.keyShortcuts)
return true
})
// iOS 8 fix
convertedCommands.append(KeyShortcut(modifiers:[.command]).uiKeyCommand)
convertedCommands.append(KeyShortcut(modifiers:[.alternate]).uiKeyCommand)
convertedCommands.append(contentsOf: shortcuts.map { $0.uiKeyCommand })
self.effectiveShortcuts = shortcuts
return convertedCommands
}
@objc func handleKeyCommand(_ command: UIKeyCommand) {
if let shortcut = findShortcut(for: command) {
shortcut.action()
}
}
private func findShortcut(for command: UIKeyCommand) -> KeyShortcut? {
if let shortcuts = self.effectiveShortcuts {
for shortcut in shortcuts {
if shortcut.isEqual(to: command) {
return shortcut
}
}
}
return nil
}
public override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if let keyCommand = sender as? UIKeyCommand, let _ = findShortcut(for: keyCommand) {
return true
} else {
return super.canPerformAction(action, withSender: sender)
}
}
public override func target(forAction action: Selector, withSender sender: Any?) -> Any? {
if let keyCommand = sender as? UIKeyCommand, let _ = findShortcut(for: keyCommand) {
return self
} else {
return super.target(forAction: action, withSender: sender)
}
}
public override var canBecomeFirstResponder: Bool {
return true
}
}

View File

@ -0,0 +1,11 @@
import Foundation
#if BUCK
import DisplayPrivate
#endif
public enum Keyboard {
public static func applyAutocorrection() {
applyKeyboardAutocorrection()
}
}

View File

@ -0,0 +1,127 @@
import Foundation
import UIKit
import AsyncDisplayKit
#if BUCK
import DisplayPrivate
#endif
struct KeyboardSurface {
let host: UIView
}
private func getFirstResponder(_ view: UIView) -> UIView? {
if view.isFirstResponder {
return view
} else {
for subview in view.subviews {
if let result = getFirstResponder(subview) {
return result
}
}
return nil
}
}
class KeyboardManager {
private let host: StatusBarHost
private weak var previousPositionAnimationMirrorSource: CATracingLayer?
private weak var previousFirstResponderView: UIView?
private var interactiveInputOffset: CGFloat = 0.0
var surfaces: [KeyboardSurface] = [] {
didSet {
self.updateSurfaces(oldValue)
}
}
init(host: StatusBarHost) {
self.host = host
}
func getCurrentKeyboardHeight() -> CGFloat {
guard let keyboardView = self.host.keyboardView else {
return 0.0
}
return keyboardView.bounds.height
}
func updateInteractiveInputOffset(_ offset: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
guard let keyboardView = self.host.keyboardView else {
return
}
self.interactiveInputOffset = offset
let previousBounds = keyboardView.bounds
let updatedBounds = CGRect(origin: CGPoint(x: 0.0, y: -offset), size: previousBounds.size)
keyboardView.layer.bounds = updatedBounds
if transition.isAnimated {
transition.animateOffsetAdditive(layer: keyboardView.layer, offset: previousBounds.minY - updatedBounds.minY, completion: completion)
} else {
completion()
}
//transition.updateSublayerTransformOffset(layer: keyboardView.layer, offset: CGPoint(x: 0.0, y: offset))
}
private func updateSurfaces(_ previousSurfaces: [KeyboardSurface]) {
guard let keyboardWindow = self.host.keyboardWindow else {
return
}
var firstResponderView: UIView?
var firstResponderDisableAutomaticKeyboardHandling: UIResponderDisableAutomaticKeyboardHandling = []
for surface in self.surfaces {
if let view = getFirstResponder(surface.host) {
firstResponderView = surface.host
firstResponderDisableAutomaticKeyboardHandling = view.disableAutomaticKeyboardHandling
break
}
}
if let firstResponderView = firstResponderView {
let containerOrigin = firstResponderView.convert(CGPoint(), to: nil)
var filteredTranslation = containerOrigin.x
if firstResponderDisableAutomaticKeyboardHandling.contains(.forward) {
filteredTranslation = max(0.0, filteredTranslation)
}
if firstResponderDisableAutomaticKeyboardHandling.contains(.backward) {
filteredTranslation = min(0.0, filteredTranslation)
}
let horizontalTranslation = CATransform3DMakeTranslation(filteredTranslation, 0.0, 0.0)
let currentTransform = keyboardWindow.layer.sublayerTransform
if !CATransform3DEqualToTransform(horizontalTranslation, currentTransform) {
//print("set to \(CGPoint(x: containerOrigin.x, y: self.interactiveInputOffset))")
keyboardWindow.layer.sublayerTransform = horizontalTranslation
}
if let tracingLayer = firstResponderView.layer as? CATracingLayer, firstResponderDisableAutomaticKeyboardHandling.isEmpty {
if let previousPositionAnimationMirrorSource = self.previousPositionAnimationMirrorSource, previousPositionAnimationMirrorSource !== tracingLayer {
previousPositionAnimationMirrorSource.setPositionAnimationMirrorTarget(nil)
}
tracingLayer.setPositionAnimationMirrorTarget(keyboardWindow.layer)
self.previousPositionAnimationMirrorSource = tracingLayer
} else if let previousPositionAnimationMirrorSource = self.previousPositionAnimationMirrorSource {
previousPositionAnimationMirrorSource.setPositionAnimationMirrorTarget(nil)
self.previousPositionAnimationMirrorSource = nil
}
} else {
keyboardWindow.layer.sublayerTransform = CATransform3DIdentity
if let previousPositionAnimationMirrorSource = self.previousPositionAnimationMirrorSource {
previousPositionAnimationMirrorSource.setPositionAnimationMirrorTarget(nil)
self.previousPositionAnimationMirrorSource = nil
}
if let previousFirstResponderView = previousFirstResponderView {
if previousFirstResponderView.window == nil {
keyboardWindow.isHidden = true
keyboardWindow.layer.cancelAnimationsRecursive(key: "position")
keyboardWindow.layer.cancelAnimationsRecursive(key: "bounds")
keyboardWindow.isHidden = false
}
}
}
self.previousFirstResponderView = firstResponderView
}
}

View File

@ -0,0 +1,10 @@
import Foundation
import UIKit
public func horizontalContainerFillingSizeForLayout(layout: ContainerViewLayout, sideInset: CGFloat) -> CGFloat {
if case .regular = layout.metrics.widthClass {
return min(layout.size.width, 414.0) - sideInset * 2.0
} else {
return layout.size.width - sideInset * 2.0
}
}

View File

@ -0,0 +1,152 @@
import Foundation
import UIKit
import AsyncDisplayKit
#if BUCK
import DisplayPrivate
#endif
public enum LegacyPresentedControllerPresentation {
case custom
case modal
}
private func passControllerAppearanceAnimated(presentation: LegacyPresentedControllerPresentation) -> Bool {
switch presentation {
case .custom:
return false
case .modal:
return true
}
}
open class LegacyPresentedController: ViewController {
private let legacyController: UIViewController
private let presentation: LegacyPresentedControllerPresentation
private var controllerNode: LegacyPresentedControllerNode {
return self.displayNode as! LegacyPresentedControllerNode
}
private var loadedController = false
var controllerLoaded: (() -> Void)?
private let asPresentable = true
public init(legacyController: UIViewController, presentation: LegacyPresentedControllerPresentation) {
self.legacyController = legacyController
self.presentation = presentation
super.init(navigationBarPresentationData: nil)
/*legacyController.navigation_setDismiss { [weak self] in
self?.dismiss()
}*/
if !asPresentable {
self.addChildViewController(legacyController)
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override open func loadDisplayNode() {
self.displayNode = LegacyPresentedControllerNode()
}
override open func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if self.ignoreAppearanceMethodInvocations() {
return
}
if !loadedController && !asPresentable {
loadedController = true
self.controllerNode.controllerView = self.legacyController.view
self.controllerNode.view.addSubview(self.legacyController.view)
self.legacyController.didMove(toParentViewController: self)
if let controllerLoaded = self.controllerLoaded {
controllerLoaded()
}
}
if !asPresentable {
self.legacyController.viewWillAppear(animated && passControllerAppearanceAnimated(presentation: self.presentation))
}
}
override open func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if self.ignoreAppearanceMethodInvocations() {
return
}
if !asPresentable {
self.legacyController.viewWillDisappear(animated && passControllerAppearanceAnimated(presentation: self.presentation))
}
}
override open func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if self.ignoreAppearanceMethodInvocations() {
return
}
if asPresentable {
if !loadedController {
loadedController = true
//self.legacyController.modalPresentationStyle = .currentContext
self.present(self.legacyController, animated: false, completion: nil)
}
} else {
switch self.presentation {
case .modal:
self.controllerNode.animateModalIn()
self.legacyController.viewDidAppear(true)
case .custom:
self.legacyController.viewDidAppear(animated)
}
}
}
override open func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if !self.asPresentable {
self.legacyController.viewDidDisappear(animated && passControllerAppearanceAnimated(presentation: self.presentation))
}
}
override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition)
}
override open func dismiss(completion: (() -> Void)? = nil) {
switch self.presentation {
case .modal:
self.controllerNode.animateModalOut { [weak self] in
/*if let controller = self?.legacyController as? TGViewController {
controller.didDismiss()
} else if let controller = self?.legacyController as? TGNavigationController {
controller.didDismiss()
}*/
self?.presentingViewController?.dismiss(animated: false, completion: completion)
}
case .custom:
/*if let controller = self.legacyController as? TGViewController {
controller.didDismiss()
} else if let controller = self.legacyController as? TGNavigationController {
controller.didDismiss()
}*/
self.presentingViewController?.dismiss(animated: false, completion: completion)
}
}
}

View File

@ -0,0 +1,40 @@
import Foundation
import UIKit
import AsyncDisplayKit
final class LegacyPresentedControllerNode: ASDisplayNode {
private var containerLayout: ContainerViewLayout?
var controllerView: UIView? {
didSet {
if let controllerView = self.controllerView, let containerLayout = self.containerLayout {
controllerView.frame = CGRect(origin: CGPoint(), size: containerLayout.size)
}
}
}
override init() {
super.init()
self.setViewBlock({
return UITracingLayerView()
})
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.containerLayout = layout
if let controllerView = self.controllerView {
controllerView.frame = CGRect(origin: CGPoint(), size: layout.size)
}
}
func animateModalIn() {
self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
}
func animateModalOut(completion: @escaping () -> Void) {
self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, completion: { _ in
completion()
})
}
}

View File

@ -0,0 +1,237 @@
import Foundation
import UIKit
import AsyncDisplayKit
private enum CornerType {
case topLeft
case topRight
case bottomLeft
case bottomRight
}
private func drawFullCorner(context: CGContext, color: UIColor, at point: CGPoint, type: CornerType, radius: CGFloat) {
context.setFillColor(color.cgColor)
switch type {
case .topLeft:
context.clear(CGRect(origin: point, size: CGSize(width: radius, height: radius)))
context.fillEllipse(in: CGRect(origin: point, size: CGSize(width: radius * 2.0, height: radius * 2.0)))
case .topRight:
context.clear(CGRect(origin: CGPoint(x: point.x - radius, y: point.y), size: CGSize(width: radius, height: radius)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
case .bottomLeft:
context.clear(CGRect(origin: CGPoint(x: point.x, y: point.y - radius), size: CGSize(width: radius, height: radius)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
case .bottomRight:
context.clear(CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius, height: radius)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
}
}
private func drawConnectingCorner(context: CGContext, color: UIColor, at point: CGPoint, type: CornerType, radius: CGFloat) {
context.setFillColor(color.cgColor)
switch type {
case .topLeft:
context.fill(CGRect(origin: CGPoint(x: point.x - radius, y: point.y), size: CGSize(width: radius, height: radius)))
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
case .topRight:
context.fill(CGRect(origin: CGPoint(x: point.x, y: point.y), size: CGSize(width: radius, height: radius)))
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
case .bottomLeft:
context.fill(CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius, height: radius)))
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
case .bottomRight:
context.fill(CGRect(origin: CGPoint(x: point.x, y: point.y - radius), size: CGSize(width: radius, height: radius)))
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
}
}
private func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, outerRadius: CGFloat, innerRadius: CGFloat) -> (CGPoint, UIImage?) {
if rects.isEmpty {
return (CGPoint(), nil)
}
var topLeft = rects[0].origin
var bottomRight = CGPoint(x: rects[0].maxX, y: rects[0].maxY)
for i in 1 ..< rects.count {
topLeft.x = min(topLeft.x, rects[i].origin.x)
topLeft.y = min(topLeft.y, rects[i].origin.y)
bottomRight.x = max(bottomRight.x, rects[i].maxX)
bottomRight.y = max(bottomRight.y, rects[i].maxY)
}
topLeft.x -= inset
topLeft.y -= inset
bottomRight.x += inset * 2.0
bottomRight.y += inset * 2.0
return (topLeft, generateImage(CGSize(width: bottomRight.x - topLeft.x, height: bottomRight.y - topLeft.y), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(color.cgColor)
context.setBlendMode(.copy)
for i in 0 ..< rects.count {
let rect = rects[i].insetBy(dx: -inset, dy: -inset)
context.fill(rect.offsetBy(dx: -topLeft.x, dy: -topLeft.y))
}
for i in 0 ..< rects.count {
let rect = rects[i].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y)
var previous: CGRect?
if i != 0 {
previous = rects[i - 1].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y)
}
var next: CGRect?
if i != rects.count - 1 {
next = rects[i + 1].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y)
}
if let previous = previous {
if previous.contains(rect.topLeft) {
if abs(rect.topLeft.x - previous.minX) >= innerRadius {
var radius = innerRadius
if let next = next {
radius = min(radius, floor((next.minY - previous.maxY) / 2.0))
}
drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.topLeft.x, y: previous.maxY), type: .topLeft, radius: radius)
}
} else {
drawFullCorner(context: context, color: color, at: rect.topLeft, type: .topLeft, radius: outerRadius)
}
if previous.contains(rect.topRight.offsetBy(dx: -1.0, dy: 0.0)) {
if abs(rect.topRight.x - previous.maxX) >= innerRadius {
var radius = innerRadius
if let next = next {
radius = min(radius, floor((next.minY - previous.maxY) / 2.0))
}
drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.topRight.x, y: previous.maxY), type: .topRight, radius: radius)
}
} else {
drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: outerRadius)
}
} else {
drawFullCorner(context: context, color: color, at: rect.topLeft, type: .topLeft, radius: outerRadius)
drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: outerRadius)
}
if let next = next {
if next.contains(rect.bottomLeft) {
if abs(rect.bottomRight.x - next.maxX) >= innerRadius {
var radius = innerRadius
if let previous = previous {
radius = min(radius, floor((next.minY - previous.maxY) / 2.0))
}
drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.bottomLeft.x, y: next.minY), type: .bottomLeft, radius: radius)
}
} else {
drawFullCorner(context: context, color: color, at: rect.bottomLeft, type: .bottomLeft, radius: outerRadius)
}
if next.contains(rect.bottomRight.offsetBy(dx: -1.0, dy: 0.0)) {
if abs(rect.bottomRight.x - next.maxX) >= innerRadius {
var radius = innerRadius
if let previous = previous {
radius = min(radius, floor((next.minY - previous.maxY) / 2.0))
}
drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.bottomRight.x, y: next.minY), type: .bottomRight, radius: radius)
}
} else {
drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: outerRadius)
}
} else {
drawFullCorner(context: context, color: color, at: rect.bottomLeft, type: .bottomLeft, radius: outerRadius)
drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: outerRadius)
}
}
}))
}
public final class LinkHighlightingNode: ASDisplayNode {
private var rects: [CGRect] = []
private let imageNode: ASImageNode
public var innerRadius: CGFloat = 4.0
public var outerRadius: CGFloat = 4.0
public var inset: CGFloat = 2.0
private var _color: UIColor
public var color: UIColor {
get {
return _color
} set(value) {
self._color = value
if !self.rects.isEmpty {
self.updateImage()
}
}
}
public init(color: UIColor) {
self._color = color
self.imageNode = ASImageNode()
self.imageNode.isUserInteractionEnabled = false
self.imageNode.displaysAsynchronously = false
self.imageNode.displayWithoutProcessing = true
super.init()
self.addSubnode(self.imageNode)
}
public func updateRects(_ rects: [CGRect]) {
if self.rects != rects {
self.rects = rects
self.updateImage()
}
}
private func updateImage() {
if rects.isEmpty {
self.imageNode.image = nil
}
let (offset, image) = generateRectsImage(color: self.color, rects: self.rects, inset: self.inset, outerRadius: self.outerRadius, innerRadius: self.innerRadius)
if let image = image {
self.imageNode.image = image
self.imageNode.frame = CGRect(origin: offset, size: image.size)
}
}
public func asyncLayout() -> (UIColor, [CGRect], CGFloat, CGFloat, CGFloat) -> () -> Void {
let currentRects = self.rects
let currentColor = self._color
let currentInnerRadius = self.innerRadius
let currentOuterRadius = self.outerRadius
let currentInset = self.inset
return { [weak self] color, rects, innerRadius, outerRadius, inset in
var updatedImage: (CGPoint, UIImage?)?
if currentRects != rects || !currentColor.isEqual(color) || currentInnerRadius != innerRadius || currentOuterRadius != outerRadius || currentInset != inset {
updatedImage = generateRectsImage(color: color, rects: rects, inset: inset, outerRadius: outerRadius, innerRadius: innerRadius)
}
return {
if let strongSelf = self {
strongSelf._color = color
strongSelf.rects = rects
strongSelf.innerRadius = innerRadius
strongSelf.outerRadius = outerRadius
strongSelf.inset = inset
if let (offset, maybeImage) = updatedImage, let image = maybeImage {
strongSelf.imageNode.image = image
strongSelf.imageNode.frame = CGRect(origin: offset, size: image.size)
}
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
import Foundation
public protocol ListViewAccessoryItem {
func isEqualToItem(_ other: ListViewAccessoryItem) -> Bool
func node(synchronous: Bool) -> ListViewAccessoryItemNode
}

View File

@ -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) {
}
}

View File

@ -0,0 +1,182 @@
import Foundation
import UIKit
#if BUCK
import DisplayPrivate
#endif
public protocol Interpolatable {
static func interpolator() -> (Interpolatable, Interpolatable, CGFloat) -> (Interpolatable)
}
private func floorToPixels(_ value: CGFloat) -> CGFloat {
return round(value * 10.0) / 10.0
}
private func floorToPixels(_ value: CGPoint) -> CGPoint {
return CGPoint(x: round(value.x * 10.0) / 10.0, y: round(value.y * 10.0) / 10.0)
}
private func floorToPixels(_ value: CGSize) -> CGSize {
return CGSize(width: round(value.width * 10.0) / 10.0, height: round(value.height * 10.0) / 10.0)
}
private func floorToPixels(_ value: CGRect) -> CGRect {
return CGRect(origin: floorToPixels(value.origin), size: floorToPixels(value.size))
}
private func floorToPixels(_ value: UIEdgeInsets) -> UIEdgeInsets {
return UIEdgeInsets(top: round(value.top * 10.0) / 10.0, left: round(value.left * 10.0) / 10.0, bottom: round(value.bottom * 10.0) / 10.0, right: round(value.right * 10.0) / 10.0)
}
extension CGFloat: Interpolatable {
public static func interpolator() -> (Interpolatable, Interpolatable, CGFloat) -> Interpolatable {
return { from, to, t -> Interpolatable in
let fromValue: CGFloat = from as! CGFloat
let toValue: CGFloat = to as! CGFloat
let invT: CGFloat = 1.0 - t
let term: CGFloat = toValue * t + fromValue * invT
return floorToPixels(term)
}
}
}
extension UIEdgeInsets: Interpolatable {
public static func interpolator() -> (Interpolatable, Interpolatable, CGFloat) -> Interpolatable {
return { from, to, t -> Interpolatable in
let fromValue = from as! UIEdgeInsets
let toValue = to as! UIEdgeInsets
return floorToPixels(UIEdgeInsets(top: toValue.top * t + fromValue.top * (1.0 - t), left: toValue.left * t + fromValue.left * (1.0 - t), bottom: toValue.bottom * t + fromValue.bottom * (1.0 - t), right: toValue.right * t + fromValue.right * (1.0 - t)))
}
}
}
extension CGRect: Interpolatable {
public static func interpolator() -> (Interpolatable, Interpolatable, CGFloat) -> Interpolatable {
return { from, to, t -> Interpolatable in
let fromValue = from as! CGRect
let toValue = to as! CGRect
return floorToPixels(CGRect(x: toValue.origin.x * t + fromValue.origin.x * (1.0 - t), y: toValue.origin.y * t + fromValue.origin.y * (1.0 - t), width: toValue.size.width * t + fromValue.size.width * (1.0 - t), height: toValue.size.height * t + fromValue.size.height * (1.0 - t)))
}
}
}
extension CGPoint: Interpolatable {
public static func interpolator() -> (Interpolatable, Interpolatable, CGFloat) -> Interpolatable {
return { from, to, t -> Interpolatable in
let fromValue = from as! CGPoint
let toValue = to as! CGPoint
return floorToPixels(CGPoint(x: toValue.x * t + fromValue.x * (1.0 - t), y: toValue.y * t + fromValue.y * (1.0 - t)))
}
}
}
private let springAnimationIn: CABasicAnimation = {
let animation = makeSpringAnimation("")
return animation
}()
private let springAnimationSolver: (CGFloat) -> CGFloat = { () -> (CGFloat) -> CGFloat in
if #available(iOS 9.0, *) {
return { t in
return springAnimationValueAt(springAnimationIn, t)
}
} else {
return { t in
return bezierPoint(0.23, 1.0, 0.32, 1.0, t)
}
}
}()
public let listViewAnimationCurveSystem: (CGFloat) -> CGFloat = { t in
return springAnimationSolver(t)
}
public let listViewAnimationCurveLinear: (CGFloat) -> CGFloat = { t in
return t
}
#if os(iOS)
public func listViewAnimationCurveFromAnimationOptions(animationOptions: UIViewAnimationOptions) -> (CGFloat) -> CGFloat {
if animationOptions.rawValue == UInt(7 << 16) {
return listViewAnimationCurveSystem
} else {
return listViewAnimationCurveLinear
}
}
#endif
public final class ListViewAnimation {
let from: Interpolatable
let to: Interpolatable
let duration: Double
let startTime: Double
private let curve: (CGFloat) -> CGFloat
private let interpolator: (Interpolatable, Interpolatable, CGFloat) -> Interpolatable
private let update: (CGFloat, Interpolatable) -> Void
private let completed: (Bool) -> Void
public init<T: Interpolatable>(from: T, to: T, duration: Double, curve: @escaping (CGFloat) -> CGFloat, beginAt: Double, update: @escaping (CGFloat, T) -> Void, completed: @escaping (Bool) -> Void = { _ in }) {
self.from = from
self.to = to
self.duration = duration
self.curve = curve
self.startTime = beginAt
self.interpolator = T.interpolator()
self.update = { progress, value in
update(progress, value as! T)
}
self.completed = completed
}
init<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.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))
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,868 @@
import Foundation
import UIKit
import SwiftSignalKit
public enum ListViewCenterScrollPositionOverflow {
case top
case bottom
}
public enum ListViewScrollPosition: Equatable {
case top(CGFloat)
case bottom(CGFloat)
case center(ListViewCenterScrollPositionOverflow)
case visible
}
public enum ListViewScrollToItemDirectionHint {
case Up
case Down
}
public enum ListViewAnimationCurve {
case Spring(duration: Double)
case Default(duration: Double?)
}
public struct ListViewScrollToItem {
public let index: Int
public let position: ListViewScrollPosition
public let animated: Bool
public let curve: ListViewAnimationCurve
public let directionHint: ListViewScrollToItemDirectionHint
public init(index: Int, position: ListViewScrollPosition, animated: Bool, curve: ListViewAnimationCurve, directionHint: ListViewScrollToItemDirectionHint) {
self.index = index
self.position = position
self.animated = animated
self.curve = curve
self.directionHint = directionHint
}
}
public enum ListViewItemOperationDirectionHint {
case Up
case Down
}
public struct ListViewDeleteItem {
public let index: Int
public let directionHint: ListViewItemOperationDirectionHint?
public init(index: Int, directionHint: ListViewItemOperationDirectionHint?) {
self.index = index
self.directionHint = directionHint
}
}
public struct ListViewInsertItem {
public let index: Int
public let previousIndex: Int?
public let item: ListViewItem
public let directionHint: ListViewItemOperationDirectionHint?
public let forceAnimateInsertion: Bool
public init(index: Int, previousIndex: Int?, item: ListViewItem, directionHint: ListViewItemOperationDirectionHint?, forceAnimateInsertion: Bool = false) {
self.index = index
self.previousIndex = previousIndex
self.item = item
self.directionHint = directionHint
self.forceAnimateInsertion = forceAnimateInsertion
}
}
public struct ListViewUpdateItem {
public let index: Int
public let previousIndex: Int
public let item: ListViewItem
public let directionHint: ListViewItemOperationDirectionHint?
public init(index: Int, previousIndex: Int, item: ListViewItem, directionHint: ListViewItemOperationDirectionHint?) {
self.index = index
self.previousIndex = previousIndex
self.item = item
self.directionHint = directionHint
}
}
public struct ListViewDeleteAndInsertOptions: OptionSet {
public let rawValue: Int
public init(rawValue: Int) {
self.rawValue = rawValue
}
public static let AnimateInsertion = ListViewDeleteAndInsertOptions(rawValue: 1)
public static let AnimateAlpha = ListViewDeleteAndInsertOptions(rawValue: 2)
public static let LowLatency = ListViewDeleteAndInsertOptions(rawValue: 4)
public static let Synchronous = ListViewDeleteAndInsertOptions(rawValue: 8)
public static let RequestItemInsertionAnimations = ListViewDeleteAndInsertOptions(rawValue: 16)
public static let AnimateTopItemPosition = ListViewDeleteAndInsertOptions(rawValue: 32)
public static let PreferSynchronousDrawing = ListViewDeleteAndInsertOptions(rawValue: 64)
public static let PreferSynchronousResourceLoading = ListViewDeleteAndInsertOptions(rawValue: 128)
public static let AnimateCrossfade = ListViewDeleteAndInsertOptions(rawValue: 256)
}
public struct ListViewUpdateSizeAndInsets {
public let size: CGSize
public let insets: UIEdgeInsets
public let headerInsets: UIEdgeInsets?
public let scrollIndicatorInsets: UIEdgeInsets?
public let duration: Double
public let curve: ListViewAnimationCurve
public let ensureTopInsetForOverlayHighlightedItems: CGFloat?
public init(size: CGSize, insets: UIEdgeInsets, headerInsets: UIEdgeInsets? = nil, scrollIndicatorInsets: UIEdgeInsets? = nil, duration: Double, curve: ListViewAnimationCurve, ensureTopInsetForOverlayHighlightedItems: CGFloat? = nil) {
self.size = size
self.insets = insets
self.headerInsets = headerInsets
self.scrollIndicatorInsets = scrollIndicatorInsets
self.duration = duration
self.curve = curve
self.ensureTopInsetForOverlayHighlightedItems = ensureTopInsetForOverlayHighlightedItems
}
}
public struct ListViewItemRange: Equatable {
public let firstIndex: Int
public let lastIndex: Int
}
public struct ListViewVisibleItemRange: Equatable {
public let firstIndex: Int
public let firstIndexFullyVisible: Bool
public let lastIndex: Int
}
public struct ListViewDisplayedItemRange: Equatable {
public let loadedRange: ListViewItemRange?
public let visibleRange: ListViewVisibleItemRange?
}
struct IndexRange {
let first: Int
let last: Int
func contains(_ index: Int) -> Bool {
return index >= first && index <= last
}
var empty: Bool {
return first > last
}
}
struct OffsetRanges {
var offsets: [(IndexRange, CGFloat)] = []
mutating func append(_ other: OffsetRanges) {
self.offsets.append(contentsOf: other.offsets)
}
mutating func offset(_ indexRange: IndexRange, offset: CGFloat) {
self.offsets.append((indexRange, offset))
}
func offsetForIndex(_ index: Int) -> CGFloat {
var result: CGFloat = 0.0
for offset in self.offsets {
if offset.0.contains(index) {
result += offset.1
}
}
return result
}
}
func binarySearch(_ inputArr: [Int], searchItem: Int) -> Int? {
var lowerIndex = 0;
var upperIndex = inputArr.count - 1
if lowerIndex > upperIndex {
return nil
}
while (true) {
let currentIndex = (lowerIndex + upperIndex) / 2
if (inputArr[currentIndex] == searchItem) {
return currentIndex
} else if (lowerIndex > upperIndex) {
return nil
} else {
if (inputArr[currentIndex] > searchItem) {
upperIndex = currentIndex - 1
} else {
lowerIndex = currentIndex + 1
}
}
}
}
struct TransactionState {
let visibleSize: CGSize
let items: [ListViewItem]
}
struct PendingNode {
let index: Int
let node: QueueLocalObject<ListViewItemNode>
let apply: () -> (Signal<Void, NoError>?, () -> Void)
let frame: CGRect
let apparentHeight: CGFloat
}
enum ListViewStateNode {
case Node(index: Int, frame: CGRect, referenceNode: QueueLocalObject<ListViewItemNode>?)
case Placeholder(frame: CGRect)
var index: Int? {
switch self {
case .Node(let index, _, _):
return index
case .Placeholder(_):
return nil
}
}
var frame: CGRect {
get {
switch self {
case .Node(_, let frame, _):
return frame
case .Placeholder(let frame):
return frame
}
} set(value) {
switch self {
case let .Node(index, _, referenceNode):
self = .Node(index: index, frame: value, referenceNode: referenceNode)
case .Placeholder(_):
self = .Placeholder(frame: value)
}
}
}
}
enum ListViewInsertionOffsetDirection {
case Up
case Down
init(_ hint: ListViewItemOperationDirectionHint) {
switch hint {
case .Up:
self = .Up
case .Down:
self = .Down
}
}
func inverted() -> ListViewInsertionOffsetDirection {
switch self {
case .Up:
return .Down
case .Down:
return .Up
}
}
}
struct ListViewInsertionPoint {
let index: Int
let point: CGPoint
let direction: ListViewInsertionOffsetDirection
}
struct ListViewState {
var insets: UIEdgeInsets
var visibleSize: CGSize
let invisibleInset: CGFloat
var nodes: [ListViewStateNode]
var scrollPosition: (Int, ListViewScrollPosition)?
var stationaryOffset: (Int, CGFloat)?
let stackFromBottom: Bool
mutating func fixScrollPosition(_ itemCount: Int) {
if let (fixedIndex, fixedPosition) = self.scrollPosition {
for node in self.nodes {
if let index = node.index, index == fixedIndex {
let offset: CGFloat
switch fixedPosition {
case let .bottom(additionalOffset):
offset = (self.visibleSize.height - self.insets.bottom) - node.frame.maxY + additionalOffset
case let .top(additionalOffset):
offset = self.insets.top - node.frame.minY + additionalOffset
case let .center(overflow):
let contentAreaHeight = self.visibleSize.height - self.insets.bottom - self.insets.top
if node.frame.size.height <= contentAreaHeight + CGFloat.ulpOfOne {
offset = self.insets.top + floor((contentAreaHeight - node.frame.size.height) / 2.0) - node.frame.minY
} else {
switch overflow {
case .top:
offset = self.insets.top - node.frame.minY
case .bottom:
offset = (self.visibleSize.height - self.insets.bottom) - node.frame.maxY
}
}
case .visible:
if node.frame.maxY > self.visibleSize.height - self.insets.bottom {
offset = (self.visibleSize.height - self.insets.bottom) - node.frame.maxY
} else if node.frame.minY < self.insets.top {
offset = self.insets.top - node.frame.minY
} else {
offset = 0.0
}
}
var minY: CGFloat = CGFloat.greatestFiniteMagnitude
var maxY: CGFloat = 0.0
for i in 0 ..< self.nodes.count {
var frame = self.nodes[i].frame
frame = frame.offsetBy(dx: 0.0, dy: offset)
self.nodes[i].frame = frame
minY = min(minY, frame.minY)
maxY = max(maxY, frame.maxY)
}
var additionalOffset: CGFloat = 0.0
if minY > self.insets.top {
additionalOffset = self.insets.top - minY
}
if abs(additionalOffset) > CGFloat.ulpOfOne {
for i in 0 ..< self.nodes.count {
var frame = self.nodes[i].frame
frame = frame.offsetBy(dx: 0.0, dy: additionalOffset)
self.nodes[i].frame = frame
}
}
self.snapToBounds(itemCount, snapTopItem: true, stackFromBottom: self.stackFromBottom)
break
}
}
} else if let (stationaryIndex, stationaryOffset) = self.stationaryOffset {
for node in self.nodes {
if node.index == stationaryIndex {
let offset = stationaryOffset - node.frame.minY
if abs(offset) > CGFloat.ulpOfOne {
for i in 0 ..< self.nodes.count {
var frame = self.nodes[i].frame
frame = frame.offsetBy(dx: 0.0, dy: offset)
self.nodes[i].frame = frame
}
}
break
}
}
}
}
mutating func setupStationaryOffset(_ index: Int, boundary: Int, frames: [Int: CGRect]) {
if index < boundary {
for node in self.nodes {
if let nodeIndex = node.index , nodeIndex >= index {
if let frame = frames[nodeIndex] {
self.stationaryOffset = (nodeIndex, frame.minY)
break
}
}
}
} else {
for node in self.nodes.reversed() {
if let nodeIndex = node.index , nodeIndex <= index {
if let frame = frames[nodeIndex] {
self.stationaryOffset = (nodeIndex, frame.minY)
break
}
}
}
}
}
mutating func snapToBounds(_ itemCount: Int, snapTopItem: Bool, stackFromBottom: Bool) {
var completeHeight: CGFloat = 0.0
var topItemFound = false
var bottomItemFound = false
var topItemEdge: CGFloat = 0.0
var bottomItemEdge: CGFloat = 0.0
for node in self.nodes {
if let index = node.index {
if index == 0 {
topItemFound = true
topItemEdge = node.frame.minY
}
break
}
}
for node in self.nodes.reversed() {
if let index = node.index {
if index == itemCount - 1 {
bottomItemFound = true
bottomItemEdge = node.frame.maxY
}
break
}
}
if topItemFound && bottomItemFound {
for node in self.nodes {
completeHeight += node.frame.size.height
}
}
let overscroll: CGFloat = 0.0
var offset: CGFloat = 0.0
if topItemFound && bottomItemFound {
let areaHeight = min(completeHeight, self.visibleSize.height - self.insets.bottom - self.insets.top)
if bottomItemEdge < self.insets.top + areaHeight - overscroll {
offset = self.insets.top + areaHeight - overscroll - bottomItemEdge
} else if topItemEdge > self.insets.top - overscroll && snapTopItem {
offset = (self.insets.top - overscroll) - topItemEdge
}
} else if topItemFound {
if topItemEdge > self.insets.top - overscroll && snapTopItem {
offset = (self.insets.top - overscroll) - topItemEdge
}
} else if bottomItemFound {
if bottomItemEdge < self.visibleSize.height - self.insets.bottom - overscroll {
offset = self.visibleSize.height - self.insets.bottom - overscroll - bottomItemEdge
}
}
if abs(offset) > CGFloat.ulpOfOne {
for i in 0 ..< self.nodes.count {
var frame = self.nodes[i].frame
frame.origin.y += offset
self.nodes[i].frame = frame
}
}
}
func insertionPoint(_ insertDirectionHints: [Int: ListViewItemOperationDirectionHint], itemCount: Int) -> ListViewInsertionPoint? {
var fixedNode: (nodeIndex: Int, index: Int, frame: CGRect)?
if let (fixedIndex, _) = self.scrollPosition {
for i in 0 ..< self.nodes.count {
let node = self.nodes[i]
if let index = node.index , index == fixedIndex {
fixedNode = (i, index, node.frame)
break
}
}
if fixedNode == nil {
return ListViewInsertionPoint(index: fixedIndex, point: CGPoint(), direction: .Down)
}
}
var fixedNodeIsStationary = false
if fixedNode == nil {
if let (fixedIndex, _) = self.stationaryOffset {
for i in 0 ..< self.nodes.count {
let node = self.nodes[i]
if let index = node.index , index == fixedIndex {
fixedNode = (i, index, node.frame)
fixedNodeIsStationary = true
break
}
}
}
}
if fixedNode == nil {
for i in 0 ..< self.nodes.count {
let node = self.nodes[i]
if let index = node.index , node.frame.maxY >= self.insets.top {
fixedNode = (i, index, node.frame)
break
}
}
}
if fixedNode == nil && self.nodes.count != 0 {
for i in (0 ..< self.nodes.count).reversed() {
let node = self.nodes[i]
if let index = node.index {
fixedNode = (i, index, node.frame)
break
}
}
}
if let fixedNode = fixedNode {
var currentUpperNode = fixedNode
for i in (0 ..< fixedNode.nodeIndex).reversed() {
let node = self.nodes[i]
if let index = node.index {
if index != currentUpperNode.index - 1 {
if currentUpperNode.frame.minY > -self.invisibleInset - CGFloat.ulpOfOne {
var directionHint: ListViewInsertionOffsetDirection?
if let hint = insertDirectionHints[currentUpperNode.index - 1] , currentUpperNode.frame.minY > self.insets.top - CGFloat.ulpOfOne {
directionHint = ListViewInsertionOffsetDirection(hint)
}
return ListViewInsertionPoint(index: currentUpperNode.index - 1, point: CGPoint(x: 0.0, y: currentUpperNode.frame.minY), direction: directionHint ?? .Up)
} else {
break
}
}
currentUpperNode = (i, index, node.frame)
}
}
if currentUpperNode.index != 0 && currentUpperNode.frame.minY > -self.invisibleInset - CGFloat.ulpOfOne {
var directionHint: ListViewInsertionOffsetDirection?
if let hint = insertDirectionHints[currentUpperNode.index - 1] {
if currentUpperNode.frame.minY >= self.insets.top - CGFloat.ulpOfOne {
directionHint = ListViewInsertionOffsetDirection(hint)
}
} else if currentUpperNode.frame.minY >= self.insets.top - CGFloat.ulpOfOne && !fixedNodeIsStationary {
directionHint = .Down
}
return ListViewInsertionPoint(index: currentUpperNode.index - 1, point: CGPoint(x: 0.0, y: currentUpperNode.frame.minY), direction: directionHint ?? .Up)
}
var currentLowerNode = fixedNode
if fixedNode.nodeIndex + 1 < self.nodes.count {
for i in (fixedNode.nodeIndex + 1) ..< self.nodes.count {
let node = self.nodes[i]
if let index = node.index {
if index != currentLowerNode.index + 1 {
if currentLowerNode.frame.maxY < self.visibleSize.height + self.invisibleInset - CGFloat.ulpOfOne {
var directionHint: ListViewInsertionOffsetDirection?
if let hint = insertDirectionHints[currentLowerNode.index + 1] , currentLowerNode.frame.maxY < self.visibleSize.height - self.insets.bottom + CGFloat.ulpOfOne {
directionHint = ListViewInsertionOffsetDirection(hint)
}
return ListViewInsertionPoint(index: currentLowerNode.index + 1, point: CGPoint(x: 0.0, y: currentLowerNode.frame.maxY), direction: directionHint ?? .Down)
} else {
break
}
}
currentLowerNode = (i, index, node.frame)
}
}
}
if currentLowerNode.index != itemCount - 1 && currentLowerNode.frame.maxY < self.visibleSize.height + self.invisibleInset - CGFloat.ulpOfOne {
var directionHint: ListViewInsertionOffsetDirection?
if let hint = insertDirectionHints[currentLowerNode.index + 1] , currentLowerNode.frame.maxY < self.visibleSize.height - self.insets.bottom + CGFloat.ulpOfOne {
directionHint = ListViewInsertionOffsetDirection(hint)
}
return ListViewInsertionPoint(index: currentLowerNode.index + 1, point: CGPoint(x: 0.0, y: currentLowerNode.frame.maxY), direction: directionHint ?? .Down)
}
} else if itemCount != 0 {
return ListViewInsertionPoint(index: 0, point: CGPoint(x: 0.0, y: self.insets.top), direction: .Down)
}
return nil
}
mutating func removeInvisibleNodes(_ operations: inout [ListViewStateOperation]) {
var i = 0
var visibleItemNodeHeight: CGFloat = 0.0
while i < self.nodes.count {
visibleItemNodeHeight += self.nodes[i].frame.height
i += 1
}
if visibleItemNodeHeight > (self.visibleSize.height + self.invisibleInset + self.invisibleInset) {
i = self.nodes.count - 1
while i >= 0 {
let itemNode = self.nodes[i]
let frame = itemNode.frame
//print("node \(i) frame \(frame)")
if frame.maxY < -self.invisibleInset || frame.origin.y > self.visibleSize.height + self.invisibleInset {
//print("remove invisible 1 \(i) frame \(frame)")
operations.append(.Remove(index: i, offsetDirection: frame.maxY < -self.invisibleInset ? .Down : .Up))
self.nodes.remove(at: i)
}
i -= 1
}
}
let upperBound = -self.invisibleInset + CGFloat.ulpOfOne
for i in 0 ..< self.nodes.count {
let node = self.nodes[i]
if let index = node.index , node.frame.maxY > upperBound {
if i != 0 {
var previousIndex = index
for j in (0 ..< i).reversed() {
if self.nodes[j].frame.maxY < upperBound {
if let index = self.nodes[j].index {
if index != previousIndex - 1 {
//print("remove monotonity \(j) (\(index))")
operations.append(.Remove(index: j, offsetDirection: .Down))
self.nodes.remove(at: j)
} else {
previousIndex = index
}
}
}
}
}
break
}
}
let lowerBound = self.visibleSize.height + self.invisibleInset - CGFloat.ulpOfOne
for i in (0 ..< self.nodes.count).reversed() {
let node = self.nodes[i]
if let index = node.index , node.frame.minY < lowerBound {
if i != self.nodes.count - 1 {
var previousIndex = index
var removeIndices: [Int] = []
for j in (i + 1) ..< self.nodes.count {
if self.nodes[j].frame.minY > lowerBound {
if let index = self.nodes[j].index {
if index != previousIndex + 1 {
removeIndices.append(j)
} else {
previousIndex = index
}
}
}
}
if !removeIndices.isEmpty {
for i in removeIndices.reversed() {
//print("remove monotonity \(i) (\(self.nodes[i].index!))")
operations.append(.Remove(index: i, offsetDirection: .Up))
self.nodes.remove(at: i)
}
}
}
break
}
}
}
func nodeInsertionPointAndIndex(_ itemIndex: Int) -> (CGPoint, Int) {
if self.nodes.count == 0 {
return (CGPoint(x: 0.0, y: self.insets.top), 0)
} else {
var index = 0
var lastNodeWithIndex = -1
for node in self.nodes {
if let nodeItemIndex = node.index {
if nodeItemIndex > itemIndex {
break
}
lastNodeWithIndex = index
}
index += 1
}
lastNodeWithIndex += 1
return (CGPoint(x: 0.0, y: lastNodeWithIndex == 0 ? self.nodes[0].frame.minY : self.nodes[lastNodeWithIndex - 1].frame.maxY), lastNodeWithIndex)
}
}
func continuousHeightRelativeToNodeIndex(_ fixedNodeIndex: Int) -> CGFloat {
let fixedIndex = self.nodes[fixedNodeIndex].index!
var height: CGFloat = 0.0
if fixedNodeIndex != 0 {
var upperIndex = fixedIndex
for i in (0 ..< fixedNodeIndex).reversed() {
if let index = self.nodes[i].index {
if index == upperIndex - 1 {
height += self.nodes[i].frame.size.height
upperIndex = index
} else {
break
}
}
}
}
if fixedNodeIndex != self.nodes.count - 1 {
var lowerIndex = fixedIndex
for i in (fixedNodeIndex + 1) ..< self.nodes.count {
if let index = self.nodes[i].index {
if index == lowerIndex + 1 {
height += self.nodes[i].frame.size.height
lowerIndex = index
} else {
break
}
}
}
}
return height
}
mutating func insertNode(_ itemIndex: Int, node: QueueLocalObject<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), at: insertionIndex)
if !animated {
switch offsetDirection {
case .Up:
var i = insertionIndex - 1
while i >= 0 {
var frame = self.nodes[i].frame
frame.origin.y -= nodeFrame.size.height
self.nodes[i].frame = frame
i -= 1
}
case .Down:
var i = insertionIndex + 1
while i < self.nodes.count {
var frame = self.nodes[i].frame
frame.origin.y += nodeFrame.size.height
self.nodes[i].frame = frame
i += 1
}
}
}
var previousIndex: Int?
for node in self.nodes {
if let index = node.index {
if let currentPreviousIndex = previousIndex {
if index <= currentPreviousIndex {
print("index <= previousIndex + 1")
break
}
previousIndex = index
} else {
previousIndex = index
}
}
}
if let _ = self.scrollPosition {
self.fixScrollPosition(itemCount)
}
}
mutating func removeNodeAtIndex(_ index: Int, direction: ListViewItemOperationDirectionHint?, animated: Bool, operations: inout [ListViewStateOperation]) {
let node = self.nodes[index]
if case let .Node(_, _, referenceNode) = node {
let nodeFrame = node.frame
self.nodes.remove(at: index)
let offsetDirection: ListViewInsertionOffsetDirection
if let direction = direction {
offsetDirection = ListViewInsertionOffsetDirection(direction)
} else {
if nodeFrame.maxY < self.insets.top + CGFloat.ulpOfOne {
offsetDirection = .Down
} else {
offsetDirection = .Up
}
}
operations.append(.Remove(index: index, offsetDirection: offsetDirection))
if let referenceNode = referenceNode , animated {
self.nodes.insert(.Placeholder(frame: nodeFrame), at: index)
operations.append(.InsertDisappearingPlaceholder(index: index, referenceNode: referenceNode, offsetDirection: offsetDirection.inverted()))
} else {
if nodeFrame.maxY > self.insets.top - CGFloat.ulpOfOne {
if let direction = direction , direction == .Down && node.frame.minY < self.visibleSize.height - self.insets.bottom + CGFloat.ulpOfOne {
for i in (0 ..< index).reversed() {
var frame = self.nodes[i].frame
frame.origin.y += nodeFrame.size.height
self.nodes[i].frame = frame
}
} else {
for i in index ..< self.nodes.count {
var frame = self.nodes[i].frame
frame.origin.y -= nodeFrame.size.height
self.nodes[i].frame = frame
}
}
} else if index != 0 {
for i in (0 ..< index).reversed() {
var frame = self.nodes[i].frame
frame.origin.y += nodeFrame.size.height
self.nodes[i].frame = frame
}
}
}
} else {
assertionFailure()
}
}
mutating func updateNodeAtItemIndex(_ itemIndex: Int, layout: ListViewItemNodeLayout, direction: ListViewItemOperationDirectionHint?, animation: ListViewItemUpdateAnimation, apply: @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void), operations: inout [ListViewStateOperation]) {
var i = -1
for node in self.nodes {
i += 1
if node.index == itemIndex {
switch animation {
case .None:
let offsetDirection: ListViewInsertionOffsetDirection
if let direction = direction {
offsetDirection = ListViewInsertionOffsetDirection(direction)
} else {
if node.frame.maxY < self.insets.top + CGFloat.ulpOfOne {
offsetDirection = .Down
} else {
offsetDirection = .Up
}
}
switch offsetDirection {
case .Up:
let offsetDelta = -(layout.size.height - node.frame.size.height)
var updatedFrame = node.frame
updatedFrame.origin.y += offsetDelta
updatedFrame.size.height = layout.size.height
self.nodes[i].frame = updatedFrame
for j in 0 ..< i {
var frame = self.nodes[j].frame
frame.origin.y += offsetDelta
self.nodes[j].frame = frame
}
case .Down:
let offsetDelta = layout.size.height - node.frame.size.height
var updatedFrame = node.frame
updatedFrame.size.height = layout.size.height
self.nodes[i].frame = updatedFrame
for j in i + 1 ..< self.nodes.count {
var frame = self.nodes[j].frame
frame.origin.y += offsetDelta
self.nodes[j].frame = frame
}
}
operations.append(.UpdateLayout(index: i, layout: layout, apply: apply))
case .System:
operations.append(.UpdateLayout(index: i, layout: layout, apply: apply))
}
break
}
}
}
}
enum ListViewStateOperation {
case InsertNode(index: Int, offsetDirection: ListViewInsertionOffsetDirection, animated: Bool, node: QueueLocalObject<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))
}

View File

@ -0,0 +1,71 @@
import Foundation
import UIKit
import SwiftSignalKit
public enum ListViewItemUpdateAnimation {
case None
case System(duration: Double)
public var isAnimated: Bool {
if case .None = self {
return false
} else {
return true
}
}
}
public struct ListViewItemConfigureNodeFlags: OptionSet {
public var rawValue: Int32
public init() {
self.rawValue = 0
}
public init(rawValue: Int32) {
self.rawValue = rawValue
}
public static let preferSynchronousResourceLoading = ListViewItemConfigureNodeFlags(rawValue: 1 << 0)
}
public struct ListViewItemApply {
public let isOnScreen: Bool
public init(isOnScreen: Bool) {
self.isOnScreen = isOnScreen
}
}
public protocol ListViewItem {
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<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) {
}
}

View File

@ -0,0 +1,152 @@
import Foundation
import UIKit
import AsyncDisplayKit
#if BUCK
import DisplayPrivate
#endif
public enum ListViewItemHeaderStickDirection {
case top
case bottom
}
public typealias ListViewItemHeaderId = Int64
public protocol ListViewItemHeader: class {
var id: ListViewItemHeaderId { get }
var stickDirection: ListViewItemHeaderStickDirection { get }
var height: CGFloat { get }
func node() -> ListViewItemHeaderNode
}
open class ListViewItemHeaderNode: ASDisplayNode {
private final var spring: ListViewItemSpring?
let wantsScrollDynamics: Bool
let isRotated: Bool
final private(set) var internalStickLocationDistanceFactor: CGFloat = 0.0
final var internalStickLocationDistance: CGFloat = 0.0
private var isFlashingOnScrolling = false
func updateInternalStickLocationDistanceFactor(_ factor: CGFloat, animated: Bool) {
self.internalStickLocationDistanceFactor = factor
}
final func updateFlashingOnScrollingInternal(_ isFlashingOnScrolling: Bool, animated: Bool) {
if self.isFlashingOnScrolling != isFlashingOnScrolling {
self.isFlashingOnScrolling = isFlashingOnScrolling
self.updateFlashingOnScrolling(isFlashingOnScrolling, animated: animated)
}
}
open func updateFlashingOnScrolling(_ isFlashingOnScrolling: Bool, animated: Bool) {
}
public init(layerBacked: Bool = false, dynamicBounce: Bool = false, isRotated: Bool = false, seeThrough: Bool = false) {
self.wantsScrollDynamics = dynamicBounce
self.isRotated = isRotated
if dynamicBounce {
self.spring = ListViewItemSpring(stiffness: -280.0, damping: -24.0, mass: 0.85)
}
if seeThrough {
if (layerBacked) {
super.init()
self.setLayerBlock({
return CASeeThroughTracingLayer()
})
} else {
super.init()
self.setViewBlock({
return CASeeThroughTracingView()
})
}
} else {
super.init()
self.isLayerBacked = layerBacked
}
}
open func updateStickDistanceFactor(_ factor: CGFloat, transition: ContainedViewLayoutTransition) {
}
final func addScrollingOffset(_ scrollingOffset: CGFloat) {
if self.spring != nil && internalStickLocationDistanceFactor.isZero {
let bounds = self.bounds
self.bounds = CGRect(origin: CGPoint(x: 0.0, y: bounds.origin.y + scrollingOffset), size: bounds.size)
}
}
public func animate(_ timestamp: Double) -> Bool {
var continueAnimations = false
if let _ = self.spring {
let bounds = self.bounds
var offset = bounds.origin.y
let currentOffset = offset
let frictionConstant: CGFloat = testSpringFriction
let springConstant: CGFloat = testSpringConstant
let time: CGFloat = 1.0 / 60.0
// friction force = velocity * friction constant
let frictionForce = self.spring!.velocity * frictionConstant
// spring force = (target point - current position) * spring constant
let springForce = -currentOffset * springConstant
// force = spring force - friction force
let force = springForce - frictionForce
// velocity = current velocity + force * time / mass
self.spring!.velocity = self.spring!.velocity + force * time
// position = current position + velocity * time
offset = currentOffset + self.spring!.velocity * time
offset = offset.isNaN ? 0.0 : offset
let epsilon: CGFloat = 0.1
if abs(offset) < epsilon && abs(self.spring!.velocity) < epsilon {
offset = 0.0
self.spring!.velocity = 0.0
} else {
continueAnimations = true
}
if abs(offset) > 250.0 {
offset = offset < 0.0 ? -250.0 : 250.0
}
self.bounds = CGRect(origin: CGPoint(x: 0.0, y: offset), size: bounds.size)
}
return continueAnimations
}
open func animateRemoved(duration: Double) {
self.alpha = 0.0
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false)
self.layer.animateScale(from: 1.0, to: 0.2, duration: duration, removeOnCompletion: false)
}
private var cachedLayout: (CGSize, CGFloat, CGFloat)?
func updateLayoutInternal(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) {
var update = false
if let cachedLayout = self.cachedLayout {
if cachedLayout.0 != size || cachedLayout.1 != leftInset || cachedLayout.2 != rightInset {
update = true
}
} else {
update = true
}
if update {
self.cachedLayout = (size, leftInset, rightInset)
self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset)
}
}
open func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) {
}
}

View File

@ -0,0 +1,549 @@
import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
#if BUCK
import DisplayPrivate
#endif
var testSpringFrictionLimits: (CGFloat, CGFloat) = (3.0, 60.0)
var testSpringFriction: CGFloat = 31.8211269378662
var testSpringConstantLimits: (CGFloat, CGFloat) = (3.0, 450.0)
var testSpringConstant: CGFloat = 443.704223632812
var testSpringResistanceFreeLimits: (CGFloat, CGFloat) = (0.05, 1.0)
var testSpringFreeResistance: CGFloat = 0.676197171211243
var testSpringResistanceScrollingLimits: (CGFloat, CGFloat) = (0.1, 1.0)
var testSpringScrollingResistance: CGFloat = 0.6721
struct ListViewItemSpring {
let stiffness: CGFloat
let damping: CGFloat
let mass: CGFloat
var velocity: CGFloat = 0.0
init(stiffness: CGFloat, damping: CGFloat, mass: CGFloat) {
self.stiffness = stiffness
self.damping = damping
self.mass = mass
}
}
public struct ListViewItemNodeLayout {
public let contentSize: CGSize
public let insets: UIEdgeInsets
public init() {
self.contentSize = CGSize()
self.insets = UIEdgeInsets()
}
public init(contentSize: CGSize, insets: UIEdgeInsets) {
self.contentSize = contentSize
self.insets = insets
}
public var size: CGSize {
return CGSize(width: self.contentSize.width + self.insets.left + self.insets.right, height: self.contentSize.height + self.insets.top + self.insets.bottom)
}
}
public enum ListViewItemNodeVisibility: Equatable {
case none
case visible(CGFloat)
public static func ==(lhs: ListViewItemNodeVisibility, rhs: ListViewItemNodeVisibility) -> Bool {
switch lhs {
case .none:
if case .none = rhs {
return true
} else {
return false
}
case let .visible(fraction):
if case .visible(fraction) = rhs {
return true
} else {
return false
}
}
}
}
public struct ListViewItemLayoutParams {
public let width: CGFloat
public let leftInset: CGFloat
public let rightInset: CGFloat
public init(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat) {
self.width = width
self.leftInset = leftInset
self.rightInset = rightInset
}
}
open class ListViewItemNode: ASDisplayNode {
let rotated: Bool
final var index: Int?
public var isHighlightedInOverlay: Bool = false
public private(set) var accessoryItemNode: ListViewAccessoryItemNode?
func setAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode?, leftInset: CGFloat, rightInset: CGFloat) {
self.accessoryItemNode = accessoryItemNode
if let accessoryItemNode = accessoryItemNode {
self.layoutAccessoryItemNode(accessoryItemNode, leftInset: leftInset, rightInset: rightInset)
}
}
final var headerAccessoryItemNode: ListViewAccessoryItemNode? {
didSet {
if let headerAccessoryItemNode = self.headerAccessoryItemNode {
self.layoutHeaderAccessoryItemNode(headerAccessoryItemNode)
}
}
}
private final var spring: ListViewItemSpring?
private final var animations: [(String, ListViewAnimation)] = []
final let wantsScrollDynamics: Bool
public final var wantsTrailingItemSpaceUpdates: Bool = false
public final var scrollPositioningInsets: UIEdgeInsets = UIEdgeInsets()
public final var canBeUsedAsScrollToItemAnchor: Bool = true
open var visibility: ListViewItemNodeVisibility = .none
open var canBeSelected: Bool {
return true
}
open var canBeLongTapped: Bool {
return false
}
open var preventsTouchesToOtherItems: Bool {
return false
}
open func touchesToOtherItemsPrevented() {
}
open func tapped() {
}
open func longTapped() {
}
public final var insets: UIEdgeInsets = UIEdgeInsets() {
didSet {
let effectiveInsets = self.insets
self.frame = CGRect(origin: self.frame.origin, size: CGSize(width: self.contentSize.width, height: self.contentSize.height + effectiveInsets.top + effectiveInsets.bottom))
let bounds = self.bounds
self.bounds = CGRect(origin: CGPoint(x: bounds.origin.x, y: -effectiveInsets.top + self.contentOffset + self.transitionOffset), size: bounds.size)
}
}
private final var _contentSize: CGSize = CGSize()
public final var contentSize: CGSize {
get {
return self._contentSize
} set(value) {
let effectiveInsets = self.insets
self.frame = CGRect(origin: self.frame.origin, size: CGSize(width: value.width, height: value.height + effectiveInsets.top + effectiveInsets.bottom))
}
}
private var contentOffset: CGFloat = 0.0 {
didSet {
let effectiveInsets = self.insets
let bounds = self.bounds
self.bounds = CGRect(origin: CGPoint(x: bounds.origin.x, y: -effectiveInsets.top + self.contentOffset + self.transitionOffset), size: bounds.size)
}
}
public var transitionOffset: CGFloat = 0.0 {
didSet {
let effectiveInsets = self.insets
let bounds = self.bounds
self.bounds = CGRect(origin: CGPoint(x: bounds.origin.x, y: -effectiveInsets.top + self.contentOffset + self.transitionOffset), size: bounds.size)
}
}
public var layout: ListViewItemNodeLayout {
var insets = self.insets
var contentSize = self.contentSize
if let animation = self.animationForKey("insets") {
insets = animation.to as! UIEdgeInsets
}
if let animation = self.animationForKey("apparentHeight") {
contentSize.height = (animation.to as! CGFloat) - insets.top - insets.bottom
}
return ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
}
public var displayResourcesReady: Signal<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
if seeThrough {
if (layerBacked) {
super.init()
self.setLayerBlock({
return CASeeThroughTracingLayer()
})
} else {
super.init()
self.setViewBlock({
return CASeeThroughTracingView()
})
}
} else {
super.init()
self.isLayerBacked = layerBacked
}
}
var apparentHeight: CGFloat = 0.0
private var _bounds: CGRect = CGRect()
private var _position: CGPoint = CGPoint()
open override var frame: CGRect {
get {
return CGRect(origin: CGPoint(x: self._position.x - self._bounds.width / 2.0, y: self._position.y - self._bounds.height / 2.0), size: self._bounds.size)
} set(value) {
let previousSize = self._bounds.size
super.frame = value
self._bounds.size = value.size
self._position = CGPoint(x: value.midX, y: value.midY)
let effectiveInsets = self.insets
self._contentSize = CGSize(width: value.size.width, height: value.size.height - effectiveInsets.top - effectiveInsets.bottom)
if previousSize != value.size {
if let headerAccessoryItemNode = self.headerAccessoryItemNode {
self.layoutHeaderAccessoryItemNode(headerAccessoryItemNode)
}
}
}
}
open override var bounds: CGRect {
get {
return self._bounds
} set(value) {
let previousSize = self._bounds.size
super.bounds = value
self._bounds = value
let effectiveInsets = self.insets
self._contentSize = CGSize(width: value.size.width, height: value.size.height - effectiveInsets.top - effectiveInsets.bottom)
if previousSize != value.size {
if let headerAccessoryItemNode = self.headerAccessoryItemNode {
self.layoutHeaderAccessoryItemNode(headerAccessoryItemNode)
}
}
}
}
public var contentBounds: CGRect {
let bounds = self.bounds
let effectiveInsets = self.insets
return CGRect(origin: CGPoint(x: 0.0, y: bounds.origin.y + effectiveInsets.top), size: CGSize(width: bounds.size.width, height: bounds.size.height - effectiveInsets.top - effectiveInsets.bottom))
}
open override var position: CGPoint {
get {
return self._position
} set(value) {
super.position = value
self._position = value
}
}
public final var apparentFrame: CGRect {
var frame = self.frame
frame.size.height = self.apparentHeight
return frame
}
public final var apparentContentFrame: CGRect {
var frame = self.frame
let insets = self.insets
frame.origin.y += insets.top
frame.size.height = self.apparentHeight - insets.top - insets.bottom
return frame
}
public final var apparentBounds: CGRect {
var bounds = self.bounds
bounds.size.height = self.apparentHeight
return bounds
}
open func layoutAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode, leftInset: CGFloat, rightInset: CGFloat) {
}
open func layoutHeaderAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) {
}
open func reuse() {
}
final func addScrollingOffset(_ scrollingOffset: CGFloat) {
if self.spring != nil {
self.contentOffset += scrollingOffset
}
}
func initializeDynamicsFromSibling(_ itemView: ListViewItemNode, additionalOffset: CGFloat) {
if let itemViewSpring = itemView.spring {
self.contentOffset = itemView.contentOffset + additionalOffset
self.spring?.velocity = itemViewSpring.velocity
}
}
public func animate(_ timestamp: Double) -> Bool {
var continueAnimations = false
if let _ = self.spring {
var offset = self.contentOffset
let frictionConstant: CGFloat = testSpringFriction
let springConstant: CGFloat = testSpringConstant
let time: CGFloat = 1.0 / 60.0
// friction force = velocity * friction constant
let frictionForce = self.spring!.velocity * frictionConstant
// spring force = (target point - current position) * spring constant
let springForce = -self.contentOffset * springConstant
// force = spring force - friction force
let force = springForce - frictionForce
// velocity = current velocity + force * time / mass
self.spring!.velocity = self.spring!.velocity + force * time
// position = current position + velocity * time
offset = self.contentOffset + self.spring!.velocity * time
offset = offset.isNaN ? 0.0 : offset
let epsilon: CGFloat = 0.1
if abs(offset) < epsilon && abs(self.spring!.velocity) < epsilon {
offset = 0.0
self.spring!.velocity = 0.0
} else {
continueAnimations = true
}
if abs(offset) > 250.0 {
offset = offset < 0.0 ? -250.0 : 250.0
}
self.contentOffset = offset
}
var i = 0
var animationCount = self.animations.count
while i < animationCount {
let (_, animation) = self.animations[i]
animation.applyAt(timestamp)
if animation.completeAt(timestamp) {
animations.remove(at: i)
animationCount -= 1
i -= 1
} else {
continueAnimations = true
}
i += 1
}
if let accessoryItemNode = self.accessoryItemNode {
if (accessoryItemNode.animate(timestamp)) {
continueAnimations = true
}
}
return continueAnimations
}
open func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
}
public func animationForKey(_ key: String) -> ListViewAnimation? {
for (animationKey, animation) in self.animations {
if animationKey == key {
return animation
}
}
return nil
}
public final func setAnimationForKey(_ key: String, animation: ListViewAnimation?) {
for i in 0 ..< self.animations.count {
let (currentKey, currentAnimation) = self.animations[i]
if currentKey == key {
self.animations.remove(at: i)
currentAnimation.cancel()
break
}
}
if let animation = animation {
self.animations.append((key, animation))
}
}
public final func removeAllAnimations() {
let previousAnimations = self.animations
self.animations.removeAll()
for (_, animation) in previousAnimations {
animation.cancel()
}
self.accessoryItemNode?.removeAllAnimations()
}
public func addInsetsAnimationToValue(_ value: UIEdgeInsets, duration: Double, beginAt: Double) {
let animation = ListViewAnimation(from: self.insets, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] _, currentValue in
if let strongSelf = self {
strongSelf.insets = currentValue
}
})
self.setAnimationForKey("insets", animation: animation)
}
public func addHeightAnimation(_ value: CGFloat, duration: Double, beginAt: Double, update: ((CGFloat, CGFloat) -> Void)? = nil) {
let animation = ListViewAnimation(from: self.bounds.height, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] progress, currentValue in
if let strongSelf = self {
let frame = strongSelf.frame
strongSelf.frame = CGRect(origin: frame.origin, size: CGSize(width: frame.width, height: currentValue))
if let update = update {
update(progress, currentValue)
}
}
})
self.setAnimationForKey("height", animation: animation)
}
func copyHeightAndApparentHeightAnimations(to otherNode: ListViewItemNode) {
if let animation = self.animationForKey("apparentHeight") {
let updatedAnimation = ListViewAnimation(copying: animation, update: { [weak otherNode] (progress: CGFloat, currentValue: CGFloat) -> Void in
if let strongSelf = otherNode {
let frame = strongSelf.frame
strongSelf.frame = CGRect(origin: frame.origin, size: CGSize(width: frame.width, height: currentValue))
}
})
otherNode.setAnimationForKey("height", animation: updatedAnimation)
}
if let animation = self.animationForKey("apparentHeight") {
let updatedAnimation = ListViewAnimation(copying: animation, update: { [weak otherNode] (progress: CGFloat, currentValue: CGFloat) -> Void in
if let strongSelf = otherNode {
strongSelf.apparentHeight = currentValue
}
})
otherNode.setAnimationForKey("apparentHeight", animation: updatedAnimation)
}
}
public func addApparentHeightAnimation(_ value: CGFloat, duration: Double, beginAt: Double, update: ((CGFloat, CGFloat) -> Void)? = nil) {
let animation = ListViewAnimation(from: self.apparentHeight, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] progress, currentValue in
if let strongSelf = self {
strongSelf.apparentHeight = currentValue
if let update = update {
update(progress, currentValue)
}
}
})
self.setAnimationForKey("apparentHeight", animation: animation)
}
public func modifyApparentHeightAnimation(_ value: CGFloat, beginAt: Double) {
if let previousAnimation = self.animationForKey("apparentHeight") {
var duration = previousAnimation.startTime + previousAnimation.duration - beginAt
if abs(self.apparentHeight - value) < CGFloat.ulpOfOne {
duration = 0.0
}
let animation = ListViewAnimation(from: self.apparentHeight, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] _, currentValue in
if let strongSelf = self {
strongSelf.apparentHeight = currentValue
}
})
self.setAnimationForKey("apparentHeight", animation: animation)
}
}
public func removeApparentHeightAnimation() {
self.setAnimationForKey("apparentHeight", animation: nil)
}
public func addTransitionOffsetAnimation(_ value: CGFloat, duration: Double, beginAt: Double) {
let animation = ListViewAnimation(from: self.transitionOffset, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] _, currentValue in
if let strongSelf = self {
strongSelf.transitionOffset = currentValue
}
})
self.setAnimationForKey("transitionOffset", animation: animation)
}
open func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
}
open func animateAdded(_ currentTimestamp: Double, duration: Double) {
}
open func animateRemoved(_ currentTimestamp: Double, duration: Double) {
}
open func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
}
open func isReorderable(at point: CGPoint) -> Bool {
return false
}
open func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) {
}
open func shouldAnimateHorizontalFrameTransition() -> Bool {
return false
}
open func header() -> ListViewItemHeader? {
return nil
}
open func updateTrailingItemSpace(_ height: CGFloat, transition: ContainedViewLayoutTransition) {
}
override open func accessibilityElementDidBecomeFocused() {
(self.supernode as? ListView)?.ensureItemNodeVisible(self, animated: false, overflow: 22.0)
}
}

View File

@ -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))
}
}

View File

@ -0,0 +1,65 @@
import Foundation
import UIKit
final class ListViewReorderingGestureRecognizer: UIGestureRecognizer {
private let shouldBegin: (CGPoint) -> Bool
private let ended: () -> Void
private let moved: (CGFloat) -> Void
private var initialLocation: CGPoint?
init(shouldBegin: @escaping (CGPoint) -> Bool, ended: @escaping () -> Void, moved: @escaping (CGFloat) -> Void) {
self.shouldBegin = shouldBegin
self.ended = ended
self.moved = moved
super.init(target: nil, action: nil)
}
override func reset() {
super.reset()
self.initialLocation = nil
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
if self.state == .possible {
if let location = touches.first?.location(in: self.view), self.shouldBegin(location) {
self.initialLocation = location
self.state = .began
} else {
self.state = .failed
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
if self.state == .began || self.state == .changed {
self.ended()
self.state = .failed
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
if self.state == .began || self.state == .changed {
self.ended()
self.state = .failed
}
}
override func touchesMoved(_ touches: Set<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)
}
}
}

View File

@ -0,0 +1,50 @@
import Foundation
import UIKit
import AsyncDisplayKit
final class ListViewReorderingItemNode: ASDisplayNode {
weak var itemNode: ListViewItemNode?
var currentState: (Int, Int)?
private let copyView: UIView?
private let initialLocation: CGPoint
init(itemNode: ListViewItemNode, initialLocation: CGPoint) {
self.itemNode = itemNode
self.copyView = itemNode.view.snapshotView(afterScreenUpdates: false)
self.initialLocation = initialLocation
super.init()
if let copyView = self.copyView {
self.view.addSubview(copyView)
copyView.frame = CGRect(origin: CGPoint(x: initialLocation.x, y: initialLocation.y), size: copyView.bounds.size)
copyView.bounds = itemNode.bounds
}
}
func updateOffset(offset: CGFloat) {
if let copyView = self.copyView {
copyView.frame = CGRect(origin: CGPoint(x: initialLocation.x, y: initialLocation.y + offset), size: copyView.bounds.size)
}
}
func currentOffset() -> CGFloat? {
if let copyView = self.copyView {
return copyView.center.y
}
return nil
}
func animateCompletion(completion: @escaping () -> Void) {
if let copyView = self.copyView, let itemNode = self.itemNode {
itemNode.isHidden = false
itemNode.transitionOffset = itemNode.apparentFrame.midY - copyView.frame.midY
itemNode.addTransitionOffsetAnimation(0.0, duration: 0.2, beginAt: CACurrentMediaTime())
completion()
} else {
completion()
}
}
}

View File

@ -0,0 +1,31 @@
import UIKit
class ListViewScroller: UIScrollView, UIGestureRecognizerDelegate {
override init(frame: CGRect) {
super.init(frame: frame)
#if os(iOS)
self.scrollsToTop = false
if #available(iOSApplicationExtension 11.0, *) {
self.contentInsetAdjustmentBehavior = .never
}
#endif
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if otherGestureRecognizer is ListViewTapGestureRecognizer {
return true
}
return false
}
#if os(iOS)
override func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
#endif
}

View File

@ -0,0 +1,6 @@
import Foundation
import UIKit
public final class ListViewTapGestureRecognizer: UITapGestureRecognizer {
}

View File

@ -0,0 +1,4 @@
import Foundation
final class ListViewTempItemNode: ListViewItemNode {
}

View File

@ -0,0 +1,65 @@
import Foundation
import UIKit
import SwiftSignalKit
public typealias ListViewTransaction = (@escaping () -> Void) -> Void
public final class ListViewTransactionQueue {
private var transactions: [ListViewTransaction] = []
public final var transactionCompleted: () -> Void = { }
public init() {
}
public func addTransaction(_ transaction: @escaping ListViewTransaction) {
precondition(Thread.isMainThread)
let beginTransaction = self.transactions.count == 0
self.transactions.append(transaction)
if beginTransaction {
transaction({ [weak self] in
precondition(Thread.isMainThread)
if Thread.isMainThread {
if let strongSelf = self {
strongSelf.endTransaction()
}
} else {
Queue.mainQueue().async {
if let strongSelf = self {
strongSelf.endTransaction()
}
}
}
})
}
}
private func endTransaction() {
precondition(Thread.isMainThread)
Queue.mainQueue().async {
self.transactionCompleted()
if !self.transactions.isEmpty {
let _ = self.transactions.removeFirst()
}
if let nextTransaction = self.transactions.first {
nextTransaction({ [weak self] in
precondition(Thread.isMainThread)
if Thread.isMainThread {
if let strongSelf = self {
strongSelf.endTransaction()
}
} else {
Queue.mainQueue().async {
if let strongSelf = self {
strongSelf.endTransaction()
}
}
}
})
}
}
}
}

View File

@ -0,0 +1,20 @@
import Foundation
import UIKit
final class MinimizeKeyboardGestureRecognizer: UISwipeGestureRecognizer, UIGestureRecognizerDelegate {
override init(target: Any?, action: Selector?) {
super.init(target: target, action: action)
self.cancelsTouchesInView = false
self.delaysTouchesBegan = false
self.delaysTouchesEnded = false
self.delegate = self
self.direction = [.left, .right]
self.numberOfTouchesRequired = 2
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}

View File

@ -0,0 +1,9 @@
#import <Foundation/Foundation.h>
@interface NSBag : NSObject
- (NSInteger)addItem:(id)item;
- (void)enumerateItems:(void (^)(id))block;
- (void)removeItem:(NSInteger)key;
@end

View File

@ -0,0 +1,64 @@
#import "NSBag.h"
@interface NSBag ()
{
NSInteger _nextKey;
NSMutableArray *_items;
NSMutableArray *_itemKeys;
}
@end
@implementation NSBag
- (instancetype)init
{
self = [super init];
if (self != nil)
{
_items = [[NSMutableArray alloc] init];
_itemKeys = [[NSMutableArray alloc] init];
}
return self;
}
- (NSInteger)addItem:(id)item
{
if (item == nil)
return -1;
NSInteger key = _nextKey;
[_items addObject:item];
[_itemKeys addObject:@(key)];
_nextKey++;
return key;
}
- (void)enumerateItems:(void (^)(id))block
{
if (block)
{
for (id item in _items)
{
block(item);
}
}
}
- (void)removeItem:(NSInteger)key
{
NSUInteger index = 0;
for (NSNumber *itemKey in _itemKeys)
{
if ([itemKey integerValue] == key)
{
[_items removeObjectAtIndex:index];
[_itemKeys removeObjectAtIndex:index];
break;
}
index++;
}
}
@end

View File

@ -0,0 +1,9 @@
#import <Foundation/Foundation.h>
@interface NSWeakReference : NSObject
@property (nonatomic, weak) id value;
- (instancetype)initWithValue:(id)value;
@end

View File

@ -0,0 +1,13 @@
#import "NSWeakReference.h"
@implementation NSWeakReference
- (instancetype)initWithValue:(id)value {
self = [super init];
if (self != nil) {
self.value = value;
}
return self;
}
@end

View File

@ -0,0 +1,417 @@
import Foundation
import UIKit
import SwiftSignalKit
private let orientationChangeDuration: Double = UIDevice.current.userInterfaceIdiom == .pad ? 0.4 : 0.3
private let defaultOrientations: UIInterfaceOrientationMask = {
if UIDevice.current.userInterfaceIdiom == .pad {
return .all
} else {
return .allButUpsideDown
}
}()
public final class PreviewingHostViewDelegate {
public let controllerForLocation: (UIView, CGPoint) -> (UIViewController, CGRect)?
public let commitController: (UIViewController) -> Void
public init(controllerForLocation: @escaping (UIView, CGPoint) -> (UIViewController, CGRect)?, commitController: @escaping (UIViewController) -> Void) {
self.controllerForLocation = controllerForLocation
self.commitController = commitController
}
}
public protocol PreviewingHostView {
@available(iOSApplicationExtension 9.0, iOS 9.0, *)
var previewingDelegate: PreviewingHostViewDelegate? { get }
}
private func tracePreviewingHostView(view: UIView, point: CGPoint) -> (UIView & PreviewingHostView, CGPoint)? {
if let view = view as? UIView & PreviewingHostView {
return (view, point)
}
if let superview = view.superview {
if let result = tracePreviewingHostView(view: superview, point: superview.convert(point, from: view)) {
return result
}
}
return nil
}
private final class WindowRootViewControllerView: UIView {
override var frame: CGRect {
get {
return super.frame
} set(value) {
var value = value
value.size.height += value.minY
value.origin.y = 0.0
super.frame = value
}
}
}
private final class WindowRootViewController: UIViewController, UIViewControllerPreviewingDelegate {
private var voiceOverStatusObserver: AnyObject?
private var registeredForPreviewing = false
var presentController: ((UIViewController, PresentationSurfaceLevel, Bool, (() -> Void)?) -> Void)?
var transitionToSize: ((CGSize, Double) -> Void)?
var orientations: UIInterfaceOrientationMask = defaultOrientations {
didSet {
if oldValue != self.orientations {
if self.orientations == .portrait {
if UIDevice.current.orientation != .portrait {
let value = UIInterfaceOrientation.portrait.rawValue
UIDevice.current.setValue(value, forKey: "orientation")
}
} else {
UIViewController.attemptRotationToDeviceOrientation()
}
}
}
}
var gestureEdges: UIRectEdge = [] {
didSet {
if oldValue != self.gestureEdges {
if #available(iOSApplicationExtension 11.0, *) {
self.setNeedsUpdateOfScreenEdgesDeferringSystemGestures()
}
}
}
}
var preferNavigationUIHidden: Bool = false {
didSet {
if oldValue != self.preferNavigationUIHidden {
if #available(iOSApplicationExtension 11.0, *) {
self.setNeedsUpdateOfHomeIndicatorAutoHidden()
}
}
}
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return .default
}
override var prefersStatusBarHidden: Bool {
return false
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return orientations
}
init() {
super.init(nibName: nil, bundle: nil)
self.extendedLayoutIncludesOpaqueBars = true
if #available(iOSApplicationExtension 11.0, *) {
self.voiceOverStatusObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.UIAccessibilityVoiceOverStatusDidChange, object: nil, queue: OperationQueue.main, using: { [weak self] _ in
if let strongSelf = self {
strongSelf.updatePreviewingRegistration()
}
})
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
if let voiceOverStatusObserver = self.voiceOverStatusObserver {
NotificationCenter.default.removeObserver(voiceOverStatusObserver)
}
}
override func preferredScreenEdgesDeferringSystemGestures() -> UIRectEdge {
return self.gestureEdges
}
override func prefersHomeIndicatorAutoHidden() -> Bool {
return self.preferNavigationUIHidden
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
UIView.performWithoutAnimation {
self.transitionToSize?(size, coordinator.transitionDuration)
}
}
override func loadView() {
self.view = WindowRootViewControllerView()
self.view.isOpaque = false
self.view.backgroundColor = nil
self.updatePreviewingRegistration()
}
private var previewingContext: AnyObject?
private func updatePreviewingRegistration() {
var shouldRegister = false
var isVoiceOverRunning = false
if #available(iOSApplicationExtension 10.0, *) {
isVoiceOverRunning = UIAccessibility.isVoiceOverRunning
}
if !isVoiceOverRunning {
shouldRegister = true
}
if shouldRegister != self.registeredForPreviewing {
self.registeredForPreviewing = shouldRegister
if shouldRegister {
if #available(iOSApplicationExtension 9.0, *) {
self.previewingContext = self.registerForPreviewing(with: self, sourceView: self.view)
}
} else if let previewingContext = self.previewingContext {
self.previewingContext = nil
if let previewingContext = previewingContext as? UIViewControllerPreviewing {
if #available(iOSApplicationExtension 9.0, *) {
self.unregisterForPreviewing(withContext: previewingContext)
}
}
}
}
}
private weak var previousPreviewingHostView: (UIView & PreviewingHostView)?
public func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? {
if UIAccessibility.isVoiceOverRunning {
return nil
}
if #available(iOSApplicationExtension 9.0, *) {
guard let result = self.view.hitTest(location, with: nil) else {
return nil
}
if let (result, resultPoint) = tracePreviewingHostView(view: result, point: self.view.convert(location, to: result)), let delegate = result.previewingDelegate {
self.previousPreviewingHostView = result
if let (controller, rect) = delegate.controllerForLocation(previewingContext.sourceView, resultPoint) {
previewingContext.sourceRect = rect
return controller
}
}
}
return nil
}
public func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController) {
if #available(iOSApplicationExtension 9.0, *) {
if let previousPreviewingHostView = self.previousPreviewingHostView, let delegate = previousPreviewingHostView.previewingDelegate {
delegate.commitController(viewControllerToCommit)
}
self.previousPreviewingHostView = nil
}
}
}
private final class NativeWindow: UIWindow, WindowHost {
var updateSize: ((CGSize) -> Void)?
var layoutSubviewsEvent: (() -> Void)?
var updateIsUpdatingOrientationLayout: ((Bool) -> Void)?
var updateToInterfaceOrientation: ((UIInterfaceOrientation) -> Void)?
var presentController: ((ContainableController, PresentationSurfaceLevel, Bool, @escaping () -> Void) -> Void)?
var presentControllerInGlobalOverlay: ((_ controller: ContainableController) -> Void)?
var hitTestImpl: ((CGPoint, UIEvent?) -> UIView?)?
var presentNativeImpl: ((UIViewController) -> Void)?
var invalidateDeferScreenEdgeGestureImpl: (() -> Void)?
var invalidatePreferNavigationUIHiddenImpl: (() -> Void)?
var cancelInteractiveKeyboardGesturesImpl: (() -> Void)?
var forEachControllerImpl: (((ContainableController) -> Void) -> Void)?
var getAccessibilityElementsImpl: (() -> [Any]?)?
override var frame: CGRect {
get {
return super.frame
} set(value) {
let sizeUpdated = super.frame.size != value.size
var frameTransition: ContainedViewLayoutTransition = .immediate
if #available(iOSApplicationExtension 9.0, *) {
let duration = UIView.inheritedAnimationDuration
if !duration.isZero {
frameTransition = .animated(duration: duration, curve: .easeInOut)
}
}
if sizeUpdated, case let .animated(duration, curve) = frameTransition {
let previousFrame = super.frame
super.frame = value
self.layer.animateFrame(from: previousFrame, to: value, duration: duration, timingFunction: curve.timingFunction)
} else {
super.frame = value
}
if sizeUpdated {
self.updateSize?(value.size)
}
}
}
override var bounds: CGRect {
get {
return super.bounds
}
set(value) {
let sizeUpdated = super.bounds.size != value.size
super.bounds = value
if sizeUpdated {
self.updateSize?(value.size)
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
if let gestureRecognizers = self.gestureRecognizers {
for recognizer in gestureRecognizers {
recognizer.delaysTouchesBegan = false
}
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
self.layoutSubviewsEvent?()
}
override func _update(toInterfaceOrientation arg1: Int32, duration arg2: Double, force arg3: Bool) {
self.updateIsUpdatingOrientationLayout?(true)
super._update(toInterfaceOrientation: arg1, duration: arg2, force: arg3)
self.updateIsUpdatingOrientationLayout?(false)
let orientation = UIInterfaceOrientation(rawValue: Int(arg1)) ?? .unknown
self.updateToInterfaceOrientation?(orientation)
}
func present(_ controller: ContainableController, on level: PresentationSurfaceLevel, blockInteraction: Bool, completion: @escaping () -> Void) {
self.presentController?(controller, level, blockInteraction, completion)
}
func presentInGlobalOverlay(_ controller: ContainableController) {
self.presentControllerInGlobalOverlay?(controller)
}
func presentNative(_ controller: UIViewController) {
self.presentNativeImpl?(controller)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return self.hitTestImpl?(point, event)
}
func invalidateDeferScreenEdgeGestures() {
self.invalidateDeferScreenEdgeGestureImpl?()
}
func invalidatePreferNavigationUIHidden() {
self.invalidatePreferNavigationUIHiddenImpl?()
}
func cancelInteractiveKeyboardGestures() {
self.cancelInteractiveKeyboardGesturesImpl?()
}
func forEachController(_ f: (ContainableController) -> Void) {
self.forEachControllerImpl?(f)
}
}
public func nativeWindowHostView() -> (UIWindow & WindowHost, WindowHostView) {
let window = NativeWindow(frame: UIScreen.main.bounds)
let rootViewController = WindowRootViewController()
window.rootViewController = rootViewController
rootViewController.viewWillAppear(false)
rootViewController.view.frame = CGRect(origin: CGPoint(), size: window.bounds.size)
rootViewController.viewDidAppear(false)
let hostView = WindowHostView(containerView: rootViewController.view, eventView: window, isRotating: {
return window.isRotating()
}, updateSupportedInterfaceOrientations: { orientations in
rootViewController.orientations = orientations
}, updateDeferScreenEdgeGestures: { edges in
rootViewController.gestureEdges = edges
}, updatePreferNavigationUIHidden: { value in
rootViewController.preferNavigationUIHidden = value
})
rootViewController.transitionToSize = { [weak hostView] size, duration in
hostView?.updateSize?(size, duration)
}
window.updateSize = { _ in
}
window.layoutSubviewsEvent = { [weak hostView] in
hostView?.layoutSubviews?()
}
window.updateIsUpdatingOrientationLayout = { [weak hostView] value in
hostView?.isUpdatingOrientationLayout = value
}
window.updateToInterfaceOrientation = { [weak hostView] orientation in
hostView?.updateToInterfaceOrientation?(orientation)
}
window.presentController = { [weak hostView] controller, level, blockInteraction, completion in
hostView?.present?(controller, level, blockInteraction, completion)
}
window.presentControllerInGlobalOverlay = { [weak hostView] controller in
hostView?.presentInGlobalOverlay?(controller)
}
window.presentNativeImpl = { [weak hostView] controller in
hostView?.presentNative?(controller)
}
window.hitTestImpl = { [weak hostView] point, event in
return hostView?.hitTest?(point, event)
}
window.invalidateDeferScreenEdgeGestureImpl = { [weak hostView] in
return hostView?.invalidateDeferScreenEdgeGesture?()
}
window.invalidatePreferNavigationUIHiddenImpl = { [weak hostView] in
return hostView?.invalidatePreferNavigationUIHidden?()
}
window.cancelInteractiveKeyboardGesturesImpl = { [weak hostView] in
hostView?.cancelInteractiveKeyboardGestures?()
}
window.forEachControllerImpl = { [weak hostView] f in
hostView?.forEachController?(f)
}
window.getAccessibilityElementsImpl = { [weak hostView] in
return hostView?.getAccessibilityElements?()
}
rootViewController.presentController = { [weak hostView] controller, level, animated, completion in
if let hostView = hostView {
hostView.present?(LegacyPresentedController(legacyController: controller, presentation: .custom), level, false, completion ?? {})
completion?()
}
}
return (window, hostView)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,149 @@
import UIKit
import AsyncDisplayKit
public class NavigationBackButtonNode: ASControlNode {
private func fontForCurrentState() -> UIFont {
return UIFont.systemFont(ofSize: 17.0)
}
private func attributesForCurrentState() -> [NSAttributedStringKey : AnyObject] {
return [
NSAttributedStringKey.font: self.fontForCurrentState(),
NSAttributedStringKey.foregroundColor: self.isEnabled ? self.color : self.disabledColor
]
}
let arrow: ASDisplayNode
let label: ASTextNode
private let arrowSpacing: CGFloat = 4.0
private var _text: String = ""
public var text: String {
get {
return self._text
}
set(value) {
self._text = value
self.label.attributedText = NSAttributedString(string: text, attributes: self.attributesForCurrentState())
self.invalidateCalculatedLayout()
}
}
public var color: UIColor = UIColor(rgb: 0x007ee5) {
didSet {
self.label.attributedText = NSAttributedString(string: self._text, attributes: self.attributesForCurrentState())
}
}
public var disabledColor: UIColor = UIColor(rgb: 0xd0d0d0) {
didSet {
self.label.attributedText = NSAttributedString(string: self._text, attributes: self.attributesForCurrentState())
}
}
private var touchCount = 0
var pressed: () -> () = {}
override public init() {
self.arrow = ASDisplayNode()
self.label = ASTextNode()
super.init()
self.isUserInteractionEnabled = true
self.isExclusiveTouch = true
self.hitTestSlop = UIEdgeInsets(top: -16.0, left: -10.0, bottom: -16.0, right: -10.0)
self.displaysAsynchronously = false
self.arrow.displaysAsynchronously = false
self.label.displaysAsynchronously = false
self.addSubnode(self.arrow)
let arrowImage = UIImage(named: "NavigationBackArrowLight", in: Bundle(for: NavigationBackButtonNode.self), compatibleWith: nil)?.precomposed()
self.arrow.contents = arrowImage?.cgImage
self.arrow.frame = CGRect(origin: CGPoint(), size: arrowImage?.size ?? CGSize())
self.addSubnode(self.label)
}
public override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
self.label.measure(CGSize(width: max(0.0, constrainedSize.width - self.arrow.frame.size.width - self.arrowSpacing), height: constrainedSize.height))
return CGSize(width: self.arrow.frame.size.width + self.arrowSpacing + self.label.calculatedSize.width, height: max(self.arrow.frame.size.height, self.label.calculatedSize.height))
}
var labelFrame: CGRect {
get {
return CGRect(x: self.arrow.frame.size.width + self.arrowSpacing, y: floor((self.frame.size.height - self.label.calculatedSize.height) / 2.0), width: self.label.calculatedSize.width, height: self.label.calculatedSize.height)
}
}
public override func layout() {
super.layout()
self.arrow.frame = CGRect(x: 0.0, y: floor((self.frame.size.height - arrow.frame.size.height) / 2.0), width: self.arrow.frame.size.width, height: self.arrow.frame.size.height)
self.label.frame = self.labelFrame
}
private func touchInsideApparentBounds(_ touch: UITouch) -> Bool {
var apparentBounds = self.bounds
let hitTestSlop = self.hitTestSlop
apparentBounds.origin.x += hitTestSlop.left
apparentBounds.size.width -= hitTestSlop.left + hitTestSlop.right
apparentBounds.origin.y += hitTestSlop.top
apparentBounds.size.height -= hitTestSlop.top + hitTestSlop.bottom
return apparentBounds.contains(touch.location(in: self.view))
}
public override func touchesBegan(_ touches: Set<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: UIViewAnimationOptions.beginFromCurrentState, animations: { () -> Void in
self.alpha = alpha
}, completion: nil)
}
else {
self.alpha = alpha
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,60 @@
import Foundation
import UIKit
import AsyncDisplayKit
final class NavigationBarBadgeNode: ASDisplayNode {
private var fillColor: UIColor
private var strokeColor: UIColor
private var textColor: UIColor
private let textNode: ASTextNode
private let backgroundNode: ASImageNode
private let font: UIFont = Font.regular(13.0)
var text: String = "" {
didSet {
self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor)
self.invalidateCalculatedLayout()
}
}
init(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) {
self.fillColor = fillColor
self.strokeColor = strokeColor
self.textColor = textColor
self.textNode = ASTextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = false
self.backgroundNode = ASImageNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: fillColor, strokeColor: strokeColor, strokeWidth: 1.0)
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.textNode)
}
func updateTheme(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) {
self.fillColor = fillColor
self.strokeColor = strokeColor
self.textColor = textColor
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: fillColor, strokeColor: strokeColor, strokeWidth: 1.0)
self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor)
}
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
let badgeSize = self.textNode.measure(constrainedSize)
let backgroundSize = CGSize(width: max(18.0, badgeSize.width + 10.0 + 1.0), height: 18.0)
let backgroundFrame = CGRect(origin: CGPoint(), size: backgroundSize)
self.backgroundNode.frame = backgroundFrame
self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(backgroundFrame.midX - badgeSize.width / 2.0), y: floorToScreenPixels((backgroundFrame.size.height - badgeSize.height) / 2.0)), size: badgeSize)
return backgroundSize
}
}

View File

@ -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) {
}
}

View File

@ -0,0 +1,7 @@
#import <UIKit/UIKit.h>
@interface NavigationBarProxy : UINavigationBar
@property (nonatomic, copy) void (^setItemsProxy)(NSArray *, NSArray *, bool);
@end

View File

@ -0,0 +1,67 @@
#import "NavigationBarProxy.h"
@interface NavigationBarProxy ()
{
NSArray *_items;
}
@end
@implementation NavigationBarProxy
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self != nil)
{
}
return self;
}
- (void)pushNavigationItem:(UINavigationItem *)item animated:(BOOL)animated
{
[self setItems:[[self items] arrayByAddingObject:item] animated:animated];
}
- (UINavigationItem *)popNavigationItemAnimated:(BOOL)animated
{
NSMutableArray *items = [[NSMutableArray alloc] initWithArray:[self items]];
UINavigationItem *lastItem = [items lastObject];
[items removeLastObject];
[self setItems:items animated:animated];
return lastItem;
}
- (UINavigationItem *)topItem
{
return [[self items] lastObject];
}
- (UINavigationItem *)backItem
{
NSLog(@"backItem");
return nil;
}
- (NSArray *)items
{
if (_items == nil)
return @[];
return _items;
}
- (void)setItems:(NSArray *)items
{
[self setItems:items animated:false];
}
- (void)setItems:(NSArray *)items animated:(BOOL)animated
{
NSArray *previousItems = _items;
_items = items;
if (_setItemsProxy)
_setItemsProxy(previousItems, items, animated);
}
@end

View File

@ -0,0 +1,6 @@
import Foundation
import AsyncDisplayKit
public protocol NavigationBarTitleTransitionNode {
func makeTransitionMirrorNode() -> ASDisplayNode
}

View File

@ -0,0 +1,8 @@
import Foundation
import UIKit
public protocol NavigationBarTitleView {
func animateLayoutTransition()
func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition)
}

View File

@ -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))
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,390 @@
import UIKit
import AsyncDisplayKit
public protocol NavigationButtonCustomDisplayNode {
var isHighlightable: Bool { get }
}
private final class NavigationButtonItemNode: ASTextNode {
private func fontForCurrentState() -> UIFont {
return self.bold ? UIFont.boldSystemFont(ofSize: 17.0) : UIFont.systemFont(ofSize: 17.0)
}
private func attributesForCurrentState() -> [NSAttributedStringKey : AnyObject] {
return [
NSAttributedStringKey.font: self.fontForCurrentState(),
NSAttributedStringKey.foregroundColor: self.isEnabled ? self.color : self.disabledColor
]
}
private var setEnabledListener: Int?
var item: UIBarButtonItem? {
didSet {
if self.item !== oldValue {
if let oldValue = oldValue, let setEnabledListener = self.setEnabledListener {
oldValue.removeSetEnabledListener(setEnabledListener)
self.setEnabledListener = nil
}
if let item = self.item {
self.setEnabledListener = item.addSetEnabledListener { [weak self] value in
self?.isEnabled = value
}
self.accessibilityHint = item.accessibilityHint
}
}
}
}
private var _text: String?
public var text: String {
get {
return _text ?? ""
}
set(value) {
_text = value
self.attributedText = NSAttributedString(string: text, attributes: self.attributesForCurrentState())
self.item?.accessibilityLabel = value
}
}
private var imageNode: ASImageNode?
private var _image: UIImage?
public var image: UIImage? {
get {
return _image
} set(value) {
_image = value
if let _ = value {
if self.imageNode == nil {
let imageNode = ASImageNode()
imageNode.displayWithoutProcessing = true
imageNode.displaysAsynchronously = false
self.imageNode = imageNode
self.addSubnode(imageNode)
}
self.imageNode?.image = image
} else if let imageNode = self.imageNode {
imageNode.removeFromSupernode()
self.imageNode = nil
}
self.invalidateCalculatedLayout()
self.setNeedsLayout()
}
}
public var node: ASDisplayNode? {
didSet {
if self.node !== oldValue {
oldValue?.removeFromSupernode()
if let node = self.node {
self.addSubnode(node)
self.invalidateCalculatedLayout()
self.setNeedsLayout()
}
}
}
}
public var color: UIColor = UIColor(rgb: 0x007ee5) {
didSet {
if let text = self._text {
self.attributedText = NSAttributedString(string: text, attributes: self.attributesForCurrentState())
}
}
}
public var disabledColor: UIColor = UIColor(rgb: 0xd0d0d0) {
didSet {
if let text = self._text {
self.attributedText = NSAttributedString(string: text, attributes: self.attributesForCurrentState())
}
}
}
private var _bold: Bool = false
public var bold: Bool {
get {
return _bold
}
set(value) {
if _bold != value {
_bold = value
self.attributedText = NSAttributedString(string: text, attributes: self.attributesForCurrentState())
}
}
}
private var touchCount = 0
public var pressed: () -> () = { }
public var highlightChanged: (Bool) -> () = { _ in }
override public var isAccessibilityElement: Bool {
get {
return true
} set(value) {
super.isAccessibilityElement = true
}
}
override public init() {
super.init()
self.isAccessibilityElement = true
self.isUserInteractionEnabled = true
self.isExclusiveTouch = true
self.hitTestSlop = UIEdgeInsets(top: -16.0, left: -10.0, bottom: -16.0, right: -10.0)
self.displaysAsynchronously = false
self.accessibilityTraits = UIAccessibilityTraitButton
}
func updateLayout(_ constrainedSize: CGSize) -> CGSize {
let superSize = super.calculateSizeThatFits(constrainedSize)
if let node = self.node {
let nodeSize = node.measure(constrainedSize)
let size = CGSize(width: max(nodeSize.width, superSize.width), height: max(nodeSize.height, superSize.height))
node.frame = CGRect(origin: CGPoint(), size: nodeSize)
return size
} else if let imageNode = self.imageNode {
let nodeSize = imageNode.image?.size ?? CGSize()
let size = CGSize(width: max(nodeSize.width, superSize.width), height: max(nodeSize.height, superSize.height))
imageNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - nodeSize.width) / 2.0) + 5.0, y: floorToScreenPixels((size.height - nodeSize.height) / 2.0)), size: nodeSize)
return size
}
return superSize
}
private func touchInsideApparentBounds(_ touch: UITouch) -> Bool {
var apparentBounds = self.bounds
let hitTestSlop = self.hitTestSlop
apparentBounds.origin.x += hitTestSlop.left
apparentBounds.size.width += -hitTestSlop.left - hitTestSlop.right
apparentBounds.origin.y += hitTestSlop.top
apparentBounds.size.height += -hitTestSlop.top - hitTestSlop.bottom
return apparentBounds.contains(touch.location(in: self.view))
}
public override func touchesBegan(_ touches: Set<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
var shouldChangeHighlight = true
if let node = self.node as? NavigationButtonCustomDisplayNode {
shouldChangeHighlight = node.isHighlightable
}
if shouldChangeHighlight {
self.alpha = !self.isEnabled ? 1.0 : (highlighted ? 0.4 : 1.0)
self.highlightChanged(highlighted)
}
}
}
public override var isEnabled: Bool {
get {
return super.isEnabled
}
set(value) {
if self.isEnabled != value {
super.isEnabled = value
self.attributedText = NSAttributedString(string: text, attributes: self.attributesForCurrentState())
}
}
}
}
final class NavigationButtonNode: ASDisplayNode {
private var nodes: [NavigationButtonItemNode] = []
public var pressed: (Int) -> () = { _ in }
public var highlightChanged: (Int, Bool) -> () = { _, _ in }
public var color: UIColor = UIColor(rgb: 0x007ee5) {
didSet {
if !self.color.isEqual(oldValue) {
for node in self.nodes {
node.color = self.color
}
}
}
}
public var disabledColor: UIColor = UIColor(rgb: 0xd0d0d0) {
didSet {
if !self.disabledColor.isEqual(oldValue) {
for node in self.nodes {
node.disabledColor = self.disabledColor
}
}
}
}
override public var accessibilityElements: [Any]? {
get {
return self.nodes
} set(value) {
}
}
override init() {
super.init()
self.isAccessibilityElement = false
}
var manualText: String {
return self.nodes.first?.text ?? ""
}
func updateManualText(_ text: String, isBack: Bool = true) {
let node: NavigationButtonItemNode
if self.nodes.count > 0 {
node = self.nodes[0]
} else {
node = NavigationButtonItemNode()
node.color = self.color
node.highlightChanged = { [weak node, weak self] value in
if let strongSelf = self, let node = node {
if let index = strongSelf.nodes.index(where: { $0 === node }) {
strongSelf.highlightChanged(index, value)
}
}
}
node.pressed = { [weak self, weak node] in
if let strongSelf = self, let node = node {
if let index = strongSelf.nodes.index(where: { $0 === node }) {
strongSelf.pressed(index)
}
}
}
self.nodes.append(node)
self.addSubnode(node)
}
node.item = nil
node.text = text
/*if isBack {
node.accessibilityHint = "Back button"
node.accessibilityTraits = 0
} else {
node.accessibilityHint = nil
node.accessibilityTraits = UIAccessibilityTraitButton
}*/
node.image = nil
node.bold = false
node.isEnabled = true
node.node = nil
if 1 < self.nodes.count {
for i in 1 ..< self.nodes.count {
self.nodes[i].removeFromSupernode()
}
self.nodes.removeSubrange(1...)
}
}
func updateItems(_ items: [UIBarButtonItem]) {
for i in 0 ..< items.count {
let node: NavigationButtonItemNode
if self.nodes.count > i {
node = self.nodes[i]
} else {
node = NavigationButtonItemNode()
node.color = self.color
node.highlightChanged = { [weak node, weak self] value in
if let strongSelf = self, let node = node {
if let index = strongSelf.nodes.index(where: { $0 === node }) {
strongSelf.highlightChanged(index, value)
}
}
}
node.pressed = { [weak self, weak node] in
if let strongSelf = self, let node = node {
if let index = strongSelf.nodes.index(where: { $0 === node }) {
strongSelf.pressed(index)
}
}
}
self.nodes.append(node)
self.addSubnode(node)
}
node.item = items[i]
node.text = items[i].title ?? ""
node.image = items[i].image
node.bold = items[i].style == .done
node.isEnabled = items[i].isEnabled
node.node = items[i].customDisplayNode
}
if items.count < self.nodes.count {
for i in items.count ..< self.nodes.count {
self.nodes[i].removeFromSupernode()
}
self.nodes.removeSubrange(items.count...)
}
}
func updateLayout(constrainedSize: CGSize) -> CGSize {
var nodeOrigin = CGPoint()
var totalSize = CGSize()
for node in self.nodes {
if !totalSize.width.isZero {
totalSize.width += 16.0
nodeOrigin.x += 16.0
}
var nodeSize = node.updateLayout(constrainedSize)
nodeSize.width = ceil(nodeSize.width)
nodeSize.height = ceil(nodeSize.height)
totalSize.width += nodeSize.width
totalSize.height = max(totalSize.height, nodeSize.height)
node.frame = CGRect(origin: nodeOrigin, size: nodeSize)
nodeOrigin.x += node.bounds.width
}
return totalSize
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
#import <UIKit/UIKit.h>
@interface NavigationControllerProxy : UINavigationController
- (instancetype)init;
@end

View File

@ -0,0 +1,15 @@
#import "NavigationControllerProxy.h"
#import "NavigationBarProxy.h"
@implementation NavigationControllerProxy
- (instancetype)init
{
self = [super initWithNavigationBarClass:[NavigationBarProxy class] toolbarClass:[UIToolbar class]];
if (self != nil) {
}
return self;
}
@end

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