Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

[DRAFT] Impression observer logic added to tiles #29919

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
luc-lisi wants to merge 12 commits into main
base: main
Choose a base branch
Loading
from impression-observer-logic

Conversation

Copy link
Collaborator

@luc-lisi luc-lisi commented Oct 8, 2025
edited
Loading

DO NOT MERGE: This is a draft PR and is still a work in progress.

The goal of this PR is to include a new impression observer class that handles its own view state using a custom UICollectionViewCell base class.

This draft has been opened for comments on the approach. Any release of this code must be done behind a feature flag.

📝 Checklist

  • I filled in the ticket numbers and a description of my work
  • I updated the PR name to follow our PR naming guidelines
  • I ensured unit tests pass and wrote tests for new code
  • If working on UI, I checked and implemented accessibility (Dynamic Text and VoiceOver)
  • If adding telemetry, I read the data stewardship requirements and will request a data review
  • If adding or modifying strings, I read the guidelines and will request a string review from l10n
  • If needed, I updated documentation and added comments to complex code

private var observedScrollViews: Set<UIScrollView> = []

private var visibleAreaFraction: CGFloat {
guard let window = window, !isHidden, alpha > 0.01, !bounds.isEmpty else { return 0 }
Copy link
Collaborator Author

@luc-lisi luc-lisi Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I got this logic right... but I would love a sanity check here as I think I've stared at this code for too long

Copy link

mobiletest-ci-bot commented Oct 8, 2025
edited
Loading

Messages
📖 Project coverage: 38.56%

🧹 Tidy commit

Just 3 file(s) touched. Thanks for keeping it clean and review-friendly!

❌ Per-file test coverage gate

The following changed file(s) are below 35.0% coverage:

File Coverage Required
firefox-ios/Client/Frontend/Home/TopSites/Cell/ObservableCollectionViewCell.swift 0.0% 35.0%
firefox-ios/Client/Frontend/Home/Homepage Rebuild/TopSites/TopSiteCell.swift 0.0% 35.0%

Client.app: Coverage: 37.15

File Coverage
ObservableCollectionViewCell.swift 0.0% ⚠️
TopSiteCell.swift 0.0% ⚠️

Generated by 🚫 Danger Swift against 728d1e0

private var inViewAreaFraction: CGFloat {
guard let window = window, !isHidden, alpha > 0.01, !bounds.isEmpty else { return 0 }
var rect = convert(bounds, to: window).intersection(window.bounds)
if rect.isNull { return 0 }
Copy link
Contributor

@justindarc justindarc Oct 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably also be a guard !rect.isNull else { return 0 } (usually guard is used for this type of early-returning)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah. Nice call!

// MARK: Visibility timer
private func startVisibilityTimerIfNeeded() {
guard visibilityTimer == nil, !wasVisibleForThisLifetime else { return }
let t = Timer(timeInterval: visibleTimeThresholdSeconds, repeats: false) { [weak self] _ in
Copy link
Contributor

@justindarc justindarc Oct 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe you can use Timer's built-in initializer instead of instantiating it with its constructor directly so you don't need to manually add it to the run loop like:

visibilityTimer = Timer.scheduledTimer(withTimeInterval: visibleTimeThresholdSeconds, repeats: false) { [weak self] _ in
 // do stuff...
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay so this was throwing me for a loop (pardon the pun).

https://developer.apple.com/documentation/foundation/timer

The docs seem to indicate that using scheduledTimer spawns the timer on the current run loop in default mode. But then these docs seem to indicate that we want to be in .common mode so that the timer can still run and fire even when the user is interacting with the UI. (If I'm understanding it correctly)

https://developer.apple.com/documentation/corefoundation/common-mode-flag

I wasn't able to obviously find a way to force the scheduledTimer into the .common mode but it does seem like there should be a way. I'll look into this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh maybe this line actually will work either way.

RunLoop.main.add(t, forMode: .common). Trying to see if there is an easy way to test this but I think it should work.

}
self.stopVisibilityTimer()
}
t.tolerance = visibleTimeThresholdSeconds * 0.1
Copy link
Contributor

@justindarc justindarc Oct 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should add this tolerance. In practice, this will just ensure that it almost always fires later than you intend.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I'm curious of your thoughts here. I was reading this:

https://developer.apple.com/library/archive/documentation/Performance/Conceptual/EnergyGuide-iOS/MinimizeTimerUse.html

And there is a section on recommending that we set tolerances to allow timers to be batched. Maybe this is an over optimization though... I don't really know.

@adudenamedruby adudenamedruby added the Do Not Merge ⛔️ This issue is a work in progress and is not ready to land label Oct 14, 2025
Copy link
Contributor

Just did a pass on this, and also asked some of the other iOS leads for feedback as well. Here's some things to think about:

  • this is going to fail on some Swiftlint stuff. For example, single letter variables, are kinda meaningless to read, and are a nono in our codebase. I'd recommend installing Swiftlint and building just to address those concerns straight out
  • missing deinit; we should have one, to guarantee observers are removed even if prepareForReuse isn’t called due to deallocation timing
  • we're observing all parent scrollviews under startObservingIfNeeded; should we only observe the parent scrollview?
  • do we need to have layoutSubviews calling startObservingIfNeeded each time? do layout updates change the scrollview hierarchy
  • there's a concern about layoutSubviews logic still firing since prepareForReuse doesn't reset isVisibilityMonitoringEnabled. The cell could theoretically be reused for a non-sponsored top site...
    • Maybe TopSiteCell in this scenario should be the one to reset isVisibilityMonitoringEnabled = false in its prepareForResuse, since it started the monitoring?
    • Logic is also duplicated between the didSet for false and the prepareForReuse
  • nit: isVisibilityMonitoringEnabled seems largely used for side effects as well as guard checks. We could think about using functions instead of variable setting side effects to initiate the visibility monitoring
  • minor performance concern: KVO fires with pixel per scroll. We colud think about doing a micro-debounce or something, perhaps?
  • nit: we could also use modern KVO tokens (NSKeyValueObservation) instead of string key paths + override observeValue to prevent crashes from mismatched add/remove and gives type safety
  • inheriting from UICollectionViewCell - what if the future version of Discover More, that we already have designs for, and will be building towards, is not in a collection view? Do we want to make this more generic, or, do we want to have a cell & a view version of this?
  • finally, and this is a bit bit of contention that's really hard to answer at the moment: we currently use Redux in the app to be doing things. So view stuff is in the view, but logic should be happening in some middleware. However, we're also currently assessing (especially next week during the work week we have), whether or not we want to continue to use Redux, and whether it's solve the problems we introduced it to solve and such. So I'm hesitating to say, "hey, this needs to go into our Redux pattern" but I'm also hesitating to say "this is fine" lol. To be discussed!?!? What's the urgency of getting this landed?

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

Reviewers

@adudenamedruby adudenamedruby Awaiting requested review from adudenamedruby

1 more reviewer

@justindarc justindarc justindarc left review comments

Reviewers whose approvals may not affect merge requirements

At least 1 approving review is required to merge this pull request.

Assignees

No one assigned

Labels

Do Not Merge ⛔️ This issue is a work in progress and is not ready to land

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

AltStyle によって変換されたページ (->オリジナル) /