Various improvements

This commit is contained in:
Ilya Laktyushin 2026-05-28 16:50:05 +02:00
parent 5fbe230308
commit 3e6363abf9
351 changed files with 11565 additions and 12031 deletions

View file

@ -88,6 +88,12 @@ final class ActionSheetControllerNode: ASDisplayNode, ASScrollViewDelegate {
}
}
override func didLoad() {
super.didLoad()
self.scrollNode.view.scrollsToTop = false
}
func performHighlightedAction() {
self.itemGroupsContainerNode.performHighlightedAction()
}

View file

@ -52,6 +52,7 @@ final class ActionSheetItemGroupNode: ASDisplayNode, ASScrollViewDelegate {
self.scrollNode.view.canCancelContentTouches = true
self.scrollNode.view.showsVerticalScrollIndicator = false
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.scrollsToTop = false
super.init()

View file

@ -72,8 +72,21 @@ public extension CALayer {
func makeAnimation(from: Any?, to: Any, keyPath: String, timingFunction: String, duration: Double, delay: Double = 0.0, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) -> CAAnimation {
if timingFunction.hasPrefix(kCAMediaTimingFunctionCustomSpringPrefix) {
let components = timingFunction.components(separatedBy: "_")
let damping = Float(components[1]) ?? 100.0
let initialVelocity = Float(components[2]) ?? 0.0
let mass: Float
let stiffness: Float
let damping: Float
let initialVelocity: Float
if components.count >= 5 {
mass = Float(components[1]) ?? 5.0
stiffness = Float(components[2]) ?? 900.0
damping = Float(components[3]) ?? 100.0
initialVelocity = Float(components[4]) ?? 0.0
} else {
mass = 5.0
stiffness = 900.0
damping = components.count > 1 ? (Float(components[1]) ?? 100.0) : 100.0
initialVelocity = components.count > 2 ? (Float(components[2]) ?? 0.0) : 0.0
}
let animation = CASpringAnimation(keyPath: keyPath)
animation.fromValue = from
@ -83,10 +96,10 @@ public extension CALayer {
if let completion = completion {
animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion)
}
animation.mass = CGFloat(mass)
animation.stiffness = CGFloat(stiffness)
animation.damping = CGFloat(damping)
animation.initialVelocity = CGFloat(initialVelocity)
animation.mass = 5.0
animation.stiffness = 900.0
animation.duration = animation.settlingDuration
animation.timingFunction = CAMediaTimingFunction.init(name: .linear)
let k = Float(UIView.animationDurationFactor())

View file

@ -14,7 +14,7 @@ public enum ContainedViewLayoutTransitionCurve: Equatable, Hashable {
case easeInOut
case easeIn
case spring
case customSpring(damping: CGFloat, initialVelocity: CGFloat)
case customSpring(mass: CGFloat = 5.0, stiffness: CGFloat = 900.0, damping: CGFloat, initialVelocity: CGFloat)
case custom(Float, Float, Float, Float)
public static var slide: ContainedViewLayoutTransitionCurve {
@ -52,8 +52,8 @@ public extension ContainedViewLayoutTransitionCurve {
return CAMediaTimingFunctionName.easeIn.rawValue
case .spring:
return kCAMediaTimingFunctionSpring
case let .customSpring(damping, initialVelocity):
return "\(kCAMediaTimingFunctionCustomSpringPrefix)_\(damping)_\(initialVelocity)"
case let .customSpring(mass, stiffness, damping, initialVelocity):
return "\(kCAMediaTimingFunctionCustomSpringPrefix)_\(mass)_\(stiffness)_\(damping)_\(initialVelocity)"
case .custom:
return CAMediaTimingFunctionName.easeInEaseOut.rawValue
}
@ -124,8 +124,8 @@ private extension CALayer {
let timingFunction: String
let mediaTimingFunction: CAMediaTimingFunction?
switch curve {
case .spring:
timingFunction = kCAMediaTimingFunctionSpring
case .spring, .customSpring:
timingFunction = curve.timingFunction
mediaTimingFunction = nil
default:
timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue

View file

@ -509,6 +509,7 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur
self.scroller.contentSize = CGSize(width: 0.0, height: infiniteScrollSize * 2.0)
self.scroller.isHidden = true
self.scroller.delegate = self.wrappedScrollViewDelegate
self.scroller.scrollsToTop = false
self.view.addSubview(self.scroller)
self.scroller.panGestureRecognizer.cancelsTouchesInView = true
self.view.addGestureRecognizer(self.scroller.panGestureRecognizer)

View file

@ -172,6 +172,7 @@ open class NavigationController: UINavigationController, ContainableController,
private var inCallStatusBar: StatusBar?
private var updateInCallStatusBarState: CallStatusBarNode?
private var globalScrollToTopNode: ScrollToTopNode?
private var windowScrollToTopProxyViews: WindowScrollToTopProxyViews?
private var rootContainer: RootContainer?
private var rootModalFrame: NavigationModalFrame?
private var modalContainers: [NavigationModalContainer] = []
@ -331,6 +332,54 @@ open class NavigationController: UINavigationController, ContainableController,
}
deinit {
self.windowScrollToTopProxyViews?.update(window: nil, mode: .disabled, referenceView: nil)
}
private func getWindowScrollToTopProxyViews() -> WindowScrollToTopProxyViews {
if let windowScrollToTopProxyViews = self.windowScrollToTopProxyViews {
return windowScrollToTopProxyViews
}
let windowScrollToTopProxyViews = WindowScrollToTopProxyViews(scrollToTop: { [weak self] subject in
self?.scrollToTop(subject)
})
self.windowScrollToTopProxyViews = windowScrollToTopProxyViews
return windowScrollToTopProxyViews
}
private func windowScrollToTopReferenceView(window: UIWindow) -> UIView? {
var view: UIView? = self.view
while let currentView = view {
if currentView.superview === window {
return currentView
}
view = currentView.superview
}
return nil
}
private func updateWindowScrollToTopProxyViews(layout: ContainerViewLayout) {
guard let window = self.view.window, let rootContainer = self.rootContainer else {
(self.globalScrollToTopNode?.view as? ScrollToTopView)?.scrollsToTop = true
self.windowScrollToTopProxyViews?.update(window: nil, mode: .disabled, referenceView: nil)
return
}
(self.globalScrollToTopNode?.view as? ScrollToTopView)?.scrollsToTop = false
let referenceView = self.windowScrollToTopReferenceView(window: window)
let proxyViews = self.getWindowScrollToTopProxyViews()
switch rootContainer {
case .flat:
let scrollToTopHeight = max(layout.statusBarHeight ?? layout.safeInsets.top, 1.0)
let frame = self.view.convert(CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: scrollToTopHeight)), to: window)
proxyViews.update(window: window, mode: .flat(frame: frame), referenceView: referenceView)
case let .split(container):
let frames = container.scrollToTopProxyFrames(layout: layout)
proxyViews.update(window: window, mode: .split(
masterFrame: container.view.convert(frames.master, to: window),
detailFrame: container.view.convert(frames.detail, to: window)
), referenceView: referenceView)
}
}
public func combinedSupportedOrientations(currentOrientationToLock: UIInterfaceOrientationMask) -> ViewControllerSupportedOrientations {
@ -498,7 +547,7 @@ open class NavigationController: UINavigationController, ContainableController,
}
if let globalScrollToTopNode = self.globalScrollToTopNode {
globalScrollToTopNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: layout.size.width, height: 1))
globalScrollToTopNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: layout.size.width, height: 1.0))
}
var overlayContainerLayout = layout
@ -1018,8 +1067,6 @@ open class NavigationController: UINavigationController, ContainableController,
case let .flat(flatContainer):
let splitContainer = NavigationSplitContainer(theme: self.theme, controllerRemoved: { [weak self] controller in
self?.controllerRemoved(controller)
}, scrollToTop: { [weak self] subject in
self?.scrollToTop(subject)
})
if let detailsPlaceholderNode = self.detailsPlaceholderNode {
self.displayNode.insertSubnode(splitContainer, aboveSubnode: detailsPlaceholderNode)
@ -1053,8 +1100,6 @@ open class NavigationController: UINavigationController, ContainableController,
} else {
let splitContainer = NavigationSplitContainer(theme: self.theme, controllerRemoved: { [weak self] controller in
self?.controllerRemoved(controller)
}, scrollToTop: { [weak self] subject in
self?.scrollToTop(subject)
})
if let detailsPlaceholderNode = self.detailsPlaceholderNode {
self.displayNode.insertSubnode(splitContainer, aboveSubnode: detailsPlaceholderNode)
@ -1385,6 +1430,8 @@ open class NavigationController: UINavigationController, ContainableController,
}
}
self.updateWindowScrollToTopProxyViews(layout: layout)
self.isUpdatingContainers = false
if notifyGlobalOverlayControllersUpdated {

View file

@ -91,6 +91,7 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
self.scrollNode.view.clipsToBounds = false
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
self.scrollNode.view.tag = 0x5C4011
self.scrollNode.view.scrollsToTop = false
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] _ in
guard let strongSelf = self, !strongSelf.isDismissed else {

View file

@ -11,8 +11,6 @@ enum NavigationSplitContainerScrollToTop {
final class NavigationSplitContainer: ASDisplayNode {
private var theme: NavigationControllerTheme
private let masterScrollToTopView: ScrollToTopView
private let detailScrollToTopView: ScrollToTopView
private let masterContainer: NavigationContainer
private let detailContainer: NavigationContainer
private let separator: ASDisplayNode
@ -39,18 +37,9 @@ final class NavigationSplitContainer: ASDisplayNode {
self.detailContainer.isInFocus = isInFocus
}
init(theme: NavigationControllerTheme, controllerRemoved: @escaping (ViewController) -> Void, scrollToTop: @escaping (NavigationSplitContainerScrollToTop) -> Void) {
init(theme: NavigationControllerTheme, controllerRemoved: @escaping (ViewController) -> Void) {
self.theme = theme
self.masterScrollToTopView = ScrollToTopView(frame: CGRect())
self.masterScrollToTopView.action = {
scrollToTop(.master)
}
self.detailScrollToTopView = ScrollToTopView(frame: CGRect())
self.detailScrollToTopView.action = {
scrollToTop(.detail)
}
self.masterContainer = NavigationContainer(isFlat: false, controllerRemoved: controllerRemoved)
self.masterContainer.clipsToBounds = true
@ -65,8 +54,6 @@ final class NavigationSplitContainer: ASDisplayNode {
self.addSubnode(self.masterContainer)
self.addSubnode(self.detailContainer)
self.addSubnode(self.separator)
self.view.addSubview(self.masterScrollToTopView)
self.view.addSubview(self.detailScrollToTopView)
}
func hasNonReadyControllers() -> Bool {
@ -83,13 +70,21 @@ final class NavigationSplitContainer: ASDisplayNode {
self.separator.backgroundColor = theme.navigationBar.separatorColor
}
func scrollToTopProxyFrames(layout: ContainerViewLayout) -> (master: CGRect, detail: CGRect) {
let masterWidth: CGFloat = min(max(320.0, floor(layout.size.width / 3.0)), floor(layout.size.width / 2.0))
let detailWidth = layout.size.width - masterWidth
let scrollToTopHeight = max(layout.statusBarHeight ?? layout.safeInsets.top, 1.0)
return (
master: CGRect(origin: CGPoint(), size: CGSize(width: masterWidth, height: scrollToTopHeight)),
detail: CGRect(origin: CGPoint(x: masterWidth, y: 0.0), size: CGSize(width: detailWidth, height: scrollToTopHeight))
)
}
func update(layout: ContainerViewLayout, masterControllers: [ViewController], detailControllers: [ViewController], detailsPlaceholderNode: NavigationDetailsPlaceholderNode?, transition: ContainedViewLayoutTransition) {
let masterWidth: CGFloat = min(max(320.0, floor(layout.size.width / 3.0)), floor(layout.size.width / 2.0))
let detailWidth = layout.size.width - masterWidth
self.masterScrollToTopView.frame = CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: masterWidth, height: 10.0))
self.detailScrollToTopView.frame = CGRect(origin: CGPoint(x: masterWidth, y: -1.0), size: CGSize(width: detailWidth, height: 1.0))
transition.updateFrame(node: self.masterContainer, frame: CGRect(origin: CGPoint(), size: CGSize(width: masterWidth, height: layout.size.height)))
transition.updateFrame(node: self.detailContainer, frame: CGRect(origin: CGPoint(x: masterWidth, y: 0.0), size: CGSize(width: detailWidth, height: layout.size.height)))
transition.updateFrame(node: self.separator, frame: CGRect(origin: CGPoint(x: masterWidth, y: 0.0), size: CGSize(width: UIScreenPixel, height: layout.size.height)))

View file

@ -35,10 +35,6 @@ class ScrollToTopView: UIScrollView, UIScrollViewDelegate {
return false
}
func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
print("scrollViewDidScrollToTop")
}
}
class ScrollToTopNode: ASDisplayNode {
@ -52,3 +48,83 @@ class ScrollToTopNode: ASDisplayNode {
})
}
}
final class WindowScrollToTopProxyViews {
enum Mode {
case disabled
case flat(frame: CGRect)
case split(masterFrame: CGRect, detailFrame: CGRect)
}
private let flatView: ScrollToTopView
private let masterView: ScrollToTopView
private let detailView: ScrollToTopView
init(scrollToTop: @escaping (NavigationSplitContainerScrollToTop) -> Void) {
self.flatView = ScrollToTopView(frame: CGRect())
self.masterView = ScrollToTopView(frame: CGRect())
self.detailView = ScrollToTopView(frame: CGRect())
self.flatView.action = {
scrollToTop(.master)
}
self.masterView.action = {
scrollToTop(.master)
}
self.detailView.action = {
scrollToTop(.detail)
}
}
deinit {
self.disable(self.flatView)
self.disable(self.masterView)
self.disable(self.detailView)
}
func update(window: UIWindow?, mode: Mode, referenceView: UIView?) {
guard let window else {
self.disable(self.flatView)
self.disable(self.masterView)
self.disable(self.detailView)
return
}
switch mode {
case .disabled:
self.disable(self.flatView)
self.disable(self.masterView)
self.disable(self.detailView)
case let .flat(frame):
self.activate(self.flatView, in: window, frame: frame, referenceView: referenceView)
self.disable(self.masterView)
self.disable(self.detailView)
case let .split(masterFrame, detailFrame):
self.disable(self.flatView)
self.activate(self.masterView, in: window, frame: masterFrame, referenceView: referenceView)
self.activate(self.detailView, in: window, frame: detailFrame, referenceView: referenceView)
}
}
private func activate(_ view: ScrollToTopView, in window: UIWindow, frame: CGRect, referenceView: UIView?) {
if let referenceView, referenceView.superview === window {
if view.superview !== window {
view.removeFromSuperview()
}
window.insertSubview(view, aboveSubview: referenceView)
} else if view.superview !== window {
view.removeFromSuperview()
window.addSubview(view)
}
view.isHidden = false
view.scrollsToTop = true
view.frame = frame
}
private func disable(_ view: ScrollToTopView) {
view.scrollsToTop = false
view.isHidden = true
view.removeFromSuperview()
}
}