Skip to content

Instantly share code, notes, and snippets.

@justindarc
Created July 7, 2025 18:59
Show Gist options
  • Save justindarc/84c5821bac80579aa56c45ecc10a95f4 to your computer and use it in GitHub Desktop.
Save justindarc/84c5821bac80579aa56c45ecc10a95f4 to your computer and use it in GitHub Desktop.
ObservableCollectionViewCell
//
// ObservableCollectionViewCell.swift
//
// Created by Justin D'Arcangelo on 7/3/25.
//
import UIKit
class ObservableCollectionViewCell: UICollectionViewCell {
var isMostlyVisible: Bool {
guard !isHidden, alpha > 0, !bounds.isEmpty, let window, window.hitTest(window.convert(center, from: superview), with: nil) == self else {
return false
}
return true
}
private var scrollViews: [UIScrollView] {
let superviews = Array(sequence(first: superview, next: { $0?.superview }))
return superviews.filter({ $0 is UIScrollView }) as? [UIScrollView] ?? []
}
@IBOutlet var label: UILabel!
override func prepareForReuse() {
for observedScrollView in observedScrollViews {
observedScrollView.removeObserver(self, forKeyPath: "contentOffset")
}
observedScrollViews.removeAll()
super.prepareForReuse()
}
override func layoutSubviews() {
for scrollView in scrollViews {
if !observedScrollViews.contains(scrollView) {
scrollView.addObserver(self, forKeyPath: "contentOffset", context: nil)
observedScrollViews.insert(scrollView)
}
}
checkVisibility()
super.layoutSubviews()
}
private var observedScrollViews: Set<UIScrollView> = []
private var wasPreviouslyMostlyVisible: Bool = false
private func checkVisibility() {
if wasPreviouslyMostlyVisible != isMostlyVisible {
print("isMostlyVisible", label.text!, isMostlyVisible)
wasPreviouslyMostlyVisible = isMostlyVisible
}
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
checkVisibility()
}
}
@justindarc
Copy link
Author

  1. layoutSubviews is invoked by the collection view when the component is "rendered" (not necessarily on-screen)
  2. when rendered, we walk up the superview chain and find all of the UIScrollView containers and observe the contentOffset property on them
  3. when any parent scroll views' contentOffset prop changes, we check if our "mostly visible" status (>50%) has changed and if so, we log it
  4. prepareForReuse is invoked by the collection view when the component is about to be recycled/un-rendered and at that point we stop observing the parent scroll views

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment