[iOS] Main-thread hang in PageChildViewController.propagateSafeArea when UIKit doesn't apply additionalSafeAreaInsets (9.0.7–10.0.0)
Les mainteneurs répondent en général sous 1 jour
Personne n'a encore pris cette issue.
Évaluation
- Difficulté
- 4/5
- Temps estimé
- 3-5 jours
- Accessibilité débutants
- 55/100
- Type d'issue
- Bug
- Clarté
- Clairement spécifiée
- Activité
- Active
- Stack technique
- ios, react-native, swift
- Domaine
- mobile, performance
Piste de recherche
Start with ios/Extensions.swift, especially PageChildViewController.propagateSafeArea() and the callbacks shown in the stack trace. Review how additionalInsets(toReach:) uses safeAreaInsets, then reproduce the described zoom push and pop sequence and check whether repeated writes stop when UIKit leaves the safe area unchanged. Done means the main-thread hang is prevented while normal safe-area propagation still works.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Description
Summary
On iOS, PageChildViewController.propagateSafeArea() in ios/Extensions.swift can loop forever on the main thread. The app stops responding, one core stays at 100%, and the iOS watchdog kills it (0x8BADF00D, either scene-update or failed-to-terminate). We see this in production TestFlight builds on 9.0.7. 9.0.11 and 10.0.0 ship the same propagateSafeArea.
Environment
| Component | Version |
|---|---|
| react-native-pager-view | 9.0.7 (same code in 9.0.11 and 10.0.0) |
| React Native | 0.88.0, Fabric |
| React | 19.3.0 |
| Expo / Expo Router | 58.0.17 / 58.0.25 |
| react-native-screens | 4.30.0 |
| Device | iPhone 16 on iOS 27.0.1; also reproduced on an iOS 27.2 Simulator |
Setup and trigger
The screen is a horizontal PagerView with three tabs, each holding a vertical list. A sticky header above the pager hides on scroll by animating translateY on the pager's container, so the pager slides up under the status bar. Tapping a list item pushes a native-stack screen with an iOS 18+ zoom transition (expo-router Link.AppleZoom). The freeze reproduces within about six cycles of: scroll the list, open an item, go back, scroll again. It happens right after going back while the header slides away.
What the loop does
Main-thread sample from the frozen Simulator app (Debug build):
PageChildViewController.viewDidLayoutSubviews() Extensions.swift:35
PageChildViewController.propagateSafeArea() Extensions.swift:64 (additionalSafeAreaInsets = insets)
-[UIViewController _updateContentOverlayInsetsForSelfAndChildren]
... -[UIView _safeAreaInsetsDidChangeFromOldInsets:]
PageChildViewController.viewSafeAreaInsetsDidChange() Extensions.swift:40
PageChildViewController.propagateSafeArea() Extensions.swift:64
... (nested again and again, then the next layout pass starts over)
At Extensions.swift:64, lldb on the same page controller shows this on every pass:
viewInWindow = {{0, 36.89}, {402, 854}} -> target top = 62 - 36.89 ≈ 25.1
view.safeAreaInsets = {62, 0, 51, 0} (never changes)
additionalSafeAreaInsets = {-152846256.6, 0, 36.77, 0}
{-152846293.5, 0, 36.99, 0}
{-152846330.4, 0, 36.88, 0} ... (−36.9 per write, forever)
additionalInsets(toReach:) derives the inherited inset as view.safeAreaInsets - additionalSafeAreaInsets, which assumes UIKit applied the last write. Here UIKit does not apply additionalSafeAreaInsets to this page at all, so view.safeAreaInsets stays at 62. Each pass therefore computes new = current + (target - 62), which moves the value by the same −36.9 pt every time. The > 0.5 "changed?" check is always true, so the write and its callbacks never stop. We don't know why UIKit stops applying the insets for this page. It only shows up after the zoom push and pop.
Before 9.x (8.0.2, UIViewRepresentable) this screen did not hang.
Patch we ship
This makes propagateSafeArea ignore its own nested callback, and write again only when the target or the page's actual safe area moved since its last write. In the normal case the first write lands and later passes have nothing to change, so behavior is unchanged. In the broken state it writes once and stops. With it, the same repro ran 20 cycles without a freeze. During 8 of those cycles, lldb counted 10 writes the new check skipped; without the patch, each of those would have started the endless loop.
@@ -21,6 +21,8 @@ struct RepresentableView: UIViewControllerRepresentable {
class PageChildViewController: UIViewController {
var wrappedView: UIView?
+ private var isPropagatingSafeArea = false
+ private var lastSafeAreaWrite: (target: UIEdgeInsets, safeArea: UIEdgeInsets)?
override func viewDidLoad() {
super.viewDidLoad()
@@ -55,14 +57,37 @@ class PageChildViewController: UIViewController {
/// Do not walk to the nearest ancestor with a non-zero inset. That source
/// is in the ancestor's own coordinate space and can track scroll offset
/// (#1099).
+ ///
+ /// `additionalInsets(toReach:)` assumes UIKit applies the value written
+ /// here. A page re-hosted by a zoom transition can keep a fixed
+ /// `safeAreaInsets` instead, so every pass subtracts the same gap again and
+ /// the main thread loops forever. Write only when the target or the page's
+ /// safe area moved since the last write.
private func propagateSafeArea() {
- let insets = additionalInsets(toReach: targetSafeAreaInsets())
- if abs(additionalSafeAreaInsets.top - insets.top) > 0.5
- || abs(additionalSafeAreaInsets.left - insets.left) > 0.5
- || abs(additionalSafeAreaInsets.bottom - insets.bottom) > 0.5
- || abs(additionalSafeAreaInsets.right - insets.right) > 0.5 {
- additionalSafeAreaInsets = insets
+ guard !isPropagatingSafeArea else {
+ return
+ }
+ let target = targetSafeAreaInsets()
+ let insets = additionalInsets(toReach: target)
+ if Self.insetsMatch(additionalSafeAreaInsets, insets) {
+ return
+ }
+ if let lastWrite = lastSafeAreaWrite,
+ Self.insetsMatch(lastWrite.target, target),
+ Self.insetsMatch(lastWrite.safeArea, view.safeAreaInsets) {
+ return
}
+ isPropagatingSafeArea = true
+ additionalSafeAreaInsets = insets
+ isPropagatingSafeArea = false
+ lastSafeAreaWrite = (target: target, safeArea: view.safeAreaInsets)
+ }
+
+ private static func insetsMatch(_ lhs: UIEdgeInsets, _ rhs: UIEdgeInsets) -> Bool {
+ abs(lhs.top - rhs.top) <= 0.5
+ && abs(lhs.left - rhs.left) <= 0.5
+ && abs(lhs.bottom - rhs.bottom) <= 0.5
+ && abs(lhs.right - rhs.right) <= 0.5
}
private func additionalInsets(toReach target: UIEdgeInsets) -> UIEdgeInsets {
We're happy to open a PR with this if the approach looks right, or to test another fix.
- Langage dominant
- TypeScript
- Étoiles
- 3.4k
- Forks
- 491
- Merge moyen
- 2 j 4 h
- PR mergées (30 j)
- 51
Préparer son environnement
- Aucun Dockerfile ni fichier Docker Compose
- Propose un modèle de pull request
- Lire le guide de contribution
Par où commencer
- Lisez l'issue en entier, puis le guide de contribution du projet.
- Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
- Forkez le dépôt et travaillez sur une branche.
- Ouvrez une pull request qui référence le numéro de l'issue.
Autres issues de callstack/react-native-pager-view
-
[iOS] UIKit's scroll to a focused field can leave the pager between pagesPeut-être pris @TaduJR l’a pris il y a 2 jours. Ouverte
Difficulté 4/5 3-5 jours Accessibilité débutants 52/100
callstack/react-native-pager-view#1230 · 4 commentaires ·
Les mainteneurs répondent en général sous 1 jour
-
[iOS] setPage does not animate when the pager resizes with the keyboardPeut-être pris @TaduJR l’a pris il y a 4 jours. Ouverte
Difficulté 4/5 3-5 jours Accessibilité débutants 52/100
callstack/react-native-pager-view#1223 · 3 commentaires ·
Les mainteneurs répondent en général sous 1 jour
-
Resolution: Needs Repro
Difficulté 4/5 3-5 jours Accessibilité débutants 32/100
callstack/react-native-pager-view#1222 · 2 commentaires · 1 réaction ·
Les mainteneurs répondent en général sous 1 jour
-
Difficulté 3/5 1-2 jours Accessibilité débutants 58/100
callstack/react-native-pager-view#1189 ·
Les mainteneurs répondent en général sous 1 jour
-
iOS: PagerScrollDelegate proxies to itself and overflows the stack when the weak originalDelegate diesPeut-être pris @lvlrSajjad l’a pris il y a 56 jours. Ouverte
Difficulté 4/5 3-5 jours Accessibilité débutants 25/100
callstack/react-native-pager-view#1146 ·
Les mainteneurs répondent en général sous 1 jour
Toutes les issues de callstack/react-native-pager-view
Issues similaires
-
Difficulté 2/5 1-3 heures Accessibilité débutants 74/100
earendil-works/pi-claude-code#36 ·
Les mainteneurs répondent en général sous 6 jours
-
Difficulté 2/5 1-3 heures Accessibilité débutants 65/100
Les mainteneurs répondent en général sous 1 jour
-
Resources: New work of jabodetabekPeut-être pris @rmt-svc l’a pris aujourd’hui. Ouverteresources
Difficulté 2/5 1-3 heures Accessibilité débutants 62/100
railmapgen/rmp-gallery#4167 ·
Les mainteneurs répondent en général sous 2 jours
-
documentation p1
Difficulté 2/5 1-3 heures Accessibilité débutants 75/100
Les mainteneurs répondent en général sous 1 jour
-
Difficulté 2/5 1-3 heures Accessibilité débutants 83/100
microsoft/monaco-editor#5577 ·
Les mainteneurs répondent en général sous 1 jour