|
|
@@ -0,0 +1,199 @@
|
|
|
+//
|
|
|
+// LNNestedScrollView.swift
|
|
|
+// Gami
|
|
|
+//
|
|
|
+// Created by OneeChan on 2026/3/12.
|
|
|
+//
|
|
|
+
|
|
|
+import Foundation
|
|
|
+import UIKit
|
|
|
+
|
|
|
+
|
|
|
+private class ScrollViewWeakWrapper {
|
|
|
+ weak var delegate: LNNestedScrollViewDelegate?
|
|
|
+ weak var childListView: UIScrollView?
|
|
|
+}
|
|
|
+
|
|
|
+private var scrollViewWeakWrapperKey: UInt8 = 0
|
|
|
+private extension UIScrollView {
|
|
|
+ var minTopOffset: CGFloat {
|
|
|
+ contentSize.height - bounds.height - contentInset.top + contentInset.bottom
|
|
|
+ }
|
|
|
+ var weakWrapper: ScrollViewWeakWrapper {
|
|
|
+ get {
|
|
|
+ var wrapper = objc_getAssociatedObject(self, &scrollViewWeakWrapperKey)
|
|
|
+ as? ScrollViewWeakWrapper
|
|
|
+ if wrapper == nil {
|
|
|
+ wrapper = ScrollViewWeakWrapper()
|
|
|
+ objc_setAssociatedObject(
|
|
|
+ self,
|
|
|
+ &scrollViewWeakWrapperKey,
|
|
|
+ wrapper,
|
|
|
+ .OBJC_ASSOCIATION_RETAIN_NONATOMIC
|
|
|
+ )
|
|
|
+ }
|
|
|
+ return wrapper!
|
|
|
+ }
|
|
|
+ set {
|
|
|
+ objc_setAssociatedObject(
|
|
|
+ self,
|
|
|
+ &scrollViewWeakWrapperKey,
|
|
|
+ newValue,
|
|
|
+ .OBJC_ASSOCIATION_RETAIN_NONATOMIC
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ weak var curChildListView: UIScrollView? {
|
|
|
+ get { weakWrapper.childListView }
|
|
|
+ set { weakWrapper.childListView = newValue }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+extension UIScrollView: LNNestedScrollViewDelegate {
|
|
|
+ func listViewDidScroll(_ scrollView: UIScrollView) -> Bool {
|
|
|
+ curChildListView = scrollView
|
|
|
+
|
|
|
+ if contentOffset.y < minTopOffset - 0.001 {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+
|
|
|
+ contentOffset.y = minTopOffset
|
|
|
+ return false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+extension UIScrollView {
|
|
|
+ weak var observerDelegate: LNNestedScrollViewDelegate? {
|
|
|
+ get { weakWrapper.delegate }
|
|
|
+ set { weakWrapper.delegate = newValue }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+protocol LNNestedScrollViewDelegate: NSObject {
|
|
|
+ func listViewDidScroll(_ scrollView: UIScrollView) -> Bool
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+private class ScrollViewDelegateProxy: NSObject, UIScrollViewDelegate {
|
|
|
+ weak var originalDelegate: UIScrollViewDelegate?
|
|
|
+ weak var observerDelegate: UIScrollViewDelegate?
|
|
|
+
|
|
|
+ func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
|
+ originalDelegate?.scrollViewDidScroll?(scrollView)
|
|
|
+ observerDelegate?.scrollViewDidScroll?(scrollView)
|
|
|
+ }
|
|
|
+
|
|
|
+ override func forwardingTarget(for aSelector: Selector!) -> Any? {
|
|
|
+ if originalDelegate?.responds(to: aSelector) == true {
|
|
|
+ return originalDelegate
|
|
|
+ }
|
|
|
+ return super.forwardingTarget(for: aSelector)
|
|
|
+ }
|
|
|
+
|
|
|
+ override func responds(to aSelector: Selector!) -> Bool {
|
|
|
+ return super.responds(to: aSelector) || originalDelegate?.responds(to: aSelector) == true
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+class LNNestedScrollView: UIScrollView, UIScrollViewDelegate, UIGestureRecognizerDelegate {
|
|
|
+ weak var scrollDelegate: UIScrollViewDelegate? {
|
|
|
+ didSet {
|
|
|
+ proxy.originalDelegate = scrollDelegate
|
|
|
+ delegate = nil
|
|
|
+ delegate = proxy
|
|
|
+ }
|
|
|
+ }
|
|
|
+ private var proxy = ScrollViewDelegateProxy()
|
|
|
+
|
|
|
+ override init(frame: CGRect) {
|
|
|
+ super.init(frame: frame)
|
|
|
+
|
|
|
+ proxy.originalDelegate = scrollDelegate
|
|
|
+ proxy.observerDelegate = self
|
|
|
+ delegate = proxy
|
|
|
+ }
|
|
|
+
|
|
|
+ required init?(coder: NSCoder) {
|
|
|
+ fatalError("init(coder:) has not been implemented")
|
|
|
+ }
|
|
|
+
|
|
|
+ func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
|
+ if observerDelegate?.listViewDidScroll(self) == true {
|
|
|
+ scrollView.contentOffset.y = -contentInset.top
|
|
|
+ }
|
|
|
+
|
|
|
+ if let curChildListView,
|
|
|
+ curChildListView.contentOffset.y > -curChildListView.contentInset.top {
|
|
|
+ scrollView.contentOffset.y = minTopOffset
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
|
+ gestureRecognizer.isKind(of: UIPanGestureRecognizer.self) && otherGestureRecognizer.isKind(of: UIPanGestureRecognizer.self)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+private class TableViewDelegateProxy: NSObject, UITableViewDelegate {
|
|
|
+ weak var originalDelegate: UITableViewDelegate?
|
|
|
+ weak var observerDelegate: UITableViewDelegate?
|
|
|
+
|
|
|
+ func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
|
+ originalDelegate?.scrollViewDidScroll?(scrollView)
|
|
|
+ observerDelegate?.scrollViewDidScroll?(scrollView)
|
|
|
+ }
|
|
|
+
|
|
|
+ override func forwardingTarget(for aSelector: Selector!) -> Any? {
|
|
|
+ if originalDelegate?.responds(to: aSelector) == true {
|
|
|
+ return originalDelegate
|
|
|
+ }
|
|
|
+ return super.forwardingTarget(for: aSelector)
|
|
|
+ }
|
|
|
+
|
|
|
+ override func responds(to aSelector: Selector!) -> Bool {
|
|
|
+ super.responds(to: aSelector)
|
|
|
+ || originalDelegate?.responds(to: aSelector) == true
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+class LNNestedTableView: UITableView, UITableViewDelegate, UIGestureRecognizerDelegate {
|
|
|
+ weak var tableDelegate: UITableViewDelegate? {
|
|
|
+ didSet {
|
|
|
+ proxy.originalDelegate = tableDelegate
|
|
|
+ delegate = nil
|
|
|
+ delegate = proxy
|
|
|
+ }
|
|
|
+ }
|
|
|
+ private var proxy = TableViewDelegateProxy()
|
|
|
+
|
|
|
+ override init(frame: CGRect, style: UITableView.Style) {
|
|
|
+ super.init(frame: frame, style: style)
|
|
|
+
|
|
|
+ proxy.originalDelegate = tableDelegate
|
|
|
+ proxy.observerDelegate = self
|
|
|
+ delegate = proxy
|
|
|
+ }
|
|
|
+
|
|
|
+ required init?(coder: NSCoder) {
|
|
|
+ fatalError("init(coder:) has not been implemented")
|
|
|
+ }
|
|
|
+
|
|
|
+ func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
|
+ if observerDelegate?.listViewDidScroll(self) == true {
|
|
|
+ scrollView.contentOffset.y = -contentInset.top
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ if let curChildListView,
|
|
|
+ curChildListView.contentOffset.y > -curChildListView.contentInset.top {
|
|
|
+ scrollView.contentOffset.y = minTopOffset
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
|
+ gestureRecognizer.isKind(of: UIPanGestureRecognizer.self) && otherGestureRecognizer.isKind(of: UIPanGestureRecognizer.self)
|
|
|
+ }
|
|
|
+}
|