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