Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions ios/PagerScrollDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,22 @@ import UIKit
Scroll delegate used to control underlying TabView's collection view.
*/
class PagerScrollDelegate: NSObject, UIScrollViewDelegate, UICollectionViewDelegate {
weak var originalDelegate: UICollectionViewDelegate?
weak var originalDelegate: UICollectionViewDelegate? {
didSet {
// Never proxy to ourselves: that makes `responds(to:)` recurse forever.
if originalDelegate === self { originalDelegate = nil }
}
}
weak var delegate: PagerViewProviderDelegate?
var orientation: UICollectionView.ScrollDirection = .horizontal

/// The collection view we already installed ourselves into. Identity, not
/// `originalDelegate == nil`, is what tells us the install already ran:
/// `originalDelegate` is weak and can go nil on its own.
weak var installedCollectionView: UICollectionView?

private var isQueryingOriginalDelegate = false

private let handledSelectors: Set<Selector> = [
#selector(scrollViewDidScroll(_:)),
#selector(scrollViewWillBeginDragging(_:)),
Expand Down Expand Up @@ -86,11 +98,25 @@ class PagerScrollDelegate: NSObject, UIScrollViewDelegate, UICollectionViewDeleg
}

override func responds(to aSelector: Selector!) -> Bool {
handledSelectors.contains(aSelector) || (originalDelegate?.responds(to: aSelector) ?? false)
if handledSelectors.contains(aSelector) { return true }

// An analytics SDK may swizzle the collection view's delegate setter and
// insert a proxy that forwards `responds(to:)` back to whatever it
// replaced - this object. Querying `originalDelegate` then re-enters this
// method through that proxy and overflows the stack. Report `false` for a
// re-entrant query so the loop terminates; UIKit skips optional delegate
// methods that answer `false`. Delegate callbacks are main-thread only.
guard !isQueryingOriginalDelegate else { return false }
isQueryingOriginalDelegate = true
defer { isQueryingOriginalDelegate = false }

return originalDelegate?.responds(to: aSelector) ?? false
}

override func forwardingTarget(for aSelector: Selector!) -> Any? {
handledSelectors.contains(aSelector) ? nil : originalDelegate
guard !handledSelectors.contains(aSelector) else { return nil }
let target = originalDelegate
return target === self ? nil : target
}
}

9 changes: 8 additions & 1 deletion ios/PagerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,18 @@ struct PagerView: View {
collectionView.showsVerticalScrollIndicator = false
collectionView.showsHorizontalScrollIndicator = false

if scrollDelegate.originalDelegate == nil {
// Install once per collection view. This closure re-runs on every
// layout pass, and `originalDelegate == nil` cannot tell "not installed
// yet" from "installed, but the weak reference died" - so once it died
// we re-adopted `collectionView.delegate`, which by then is this object
// (or a proxy forwarding back to it). Either way the delegate chain
// becomes a cycle and recurses until the stack overflows.
if scrollDelegate.installedCollectionView !== collectionView {
scrollDelegate.originalDelegate = collectionView.delegate
scrollDelegate.delegate = delegate
// VTabView-style rotation preserves TabView's horizontal collection view.
scrollDelegate.orientation = .horizontal
scrollDelegate.installedCollectionView = collectionView
collectionView.delegate = scrollDelegate
}
}
Expand Down