Skip to content
Merged
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
28 changes: 28 additions & 0 deletions android/src/main/java/com/sheet2/AppFittedSheet.kt
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,12 @@ open class AppFittedSheet(context: Context) : ViewGroup(context), LifecycleEvent
isSystemUILight = isSystemUILight
) { dismissAll ->
val parent = mHostView.parent as? ViewGroup
SheetTreeLog.log(
context,
"sheet.detachHost",
"host=${SheetTreeLog.tag(mHostView)} parent=${SheetTreeLog.tag(parent)}" +
" parentChildren=${SheetTreeLog.childTags(parent)}",
)
parent?.removeViewAt(0)
onSheetDismiss()
if (dismissAll) {
Expand Down Expand Up @@ -262,6 +268,16 @@ open class AppFittedSheet(context: Context) : ViewGroup(context), LifecycleEvent

override fun addView(child: View, index: Int) {
UiThreadUtil.assertOnUiThread()
// An overlay child is kept out of mHostView while getChildCount keeps
// counting it, so from here on this view's children and React Native's idea
// of them are two different lists.
SheetTreeLog.log(
context,
"sheet.addView",
"child=${SheetTreeLog.tag(child)} index=$index self=${SheetTreeLog.tag(this)}" +
" hostChildren=${SheetTreeLog.childTags(mHostView)}" +
" inline=$useInlinePresentation",
)
if (useInlinePresentation && index > 0) {
inlineOverlayView = child
inlinePresenter.setOverlayView(child)
Expand Down Expand Up @@ -289,6 +305,12 @@ open class AppFittedSheet(context: Context) : ViewGroup(context), LifecycleEvent

override fun removeView(child: View) {
UiThreadUtil.assertOnUiThread()
SheetTreeLog.log(
context,
"sheet.removeView",
"child=${SheetTreeLog.tag(child)} self=${SheetTreeLog.tag(this)}" +
" hostChildren=${SheetTreeLog.childTags(mHostView)}",
)
if (child == inlineOverlayView) {
inlineOverlayView = null
inlinePresenter.setOverlayView(null)
Expand All @@ -304,6 +326,12 @@ open class AppFittedSheet(context: Context) : ViewGroup(context), LifecycleEvent

override fun removeViewAt(index: Int) {
UiThreadUtil.assertOnUiThread()
SheetTreeLog.log(
context,
"sheet.removeViewAt",
"index=$index self=${SheetTreeLog.tag(this)}" +
" hostChildren=${SheetTreeLog.childTags(mHostView)}",
)
if (useInlinePresentation && index >= mHostView.childCount && inlineOverlayView != null) {
inlineOverlayView = null
inlinePresenter.setOverlayView(null)
Expand Down
26 changes: 22 additions & 4 deletions android/src/main/java/com/sheet2/DialogRootViewGroup.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import android.graphics.Outline
import android.os.Looper
import android.view.View
import android.view.ViewOutlineProvider
import com.facebook.react.uimanager.PixelUtil.pxToDp
import kotlin.math.max
import kotlin.math.min

Expand Down Expand Up @@ -86,7 +85,6 @@ class DialogRootViewGroup(context: Context) : BaseRNView(context) {
if (sheetMaxHeightSize == Float.MAX_VALUE) return
val newHeight = allowedHeight
val newWidth = allowedWidth
println("😀 DialogRootViewGroup.setVirtualHeight ${newHeight.pxToDp()} :${newWidth.pxToDp()}")
ensureLayoutParams()
layoutParams?.height = newHeight
translationX = centeringTranslationX(newWidth)
Expand All @@ -105,19 +103,39 @@ class DialogRootViewGroup(context: Context) : BaseRNView(context) {
}

override fun addView(child: View, index: Int, params: LayoutParams) {
println("😀 DialogRootViewGroup.addView ${child.id}")
if (reactView != null) removeView(reactView)
// Dropping the previous child here is a removal React Native never hears
// about: it goes on counting that child, and its next insert lands at an
// index the group no longer has.
val replaced = reactView
SheetTreeLog.log(
context,
"host.addView",
"child=${SheetTreeLog.tag(child)} index=$index children=${SheetTreeLog.childTags(this)}" +
" replaces=${SheetTreeLog.tag(replaced)}",
)
if (replaced != null) removeView(replaced)
super.addView(child, index, params)
reactView = child
setVirtualHeight(sheetMaxHeightSize)
}

override fun removeView(view: View?) {
SheetTreeLog.log(
context,
"host.removeView",
"child=${SheetTreeLog.tag(view)} children=${SheetTreeLog.childTags(this)}",
)
if (view == reactView) releaseReactView()
super.removeView(view)
}

override fun removeViewAt(index: Int) {
SheetTreeLog.log(
context,
"host.removeViewAt",
"index=$index child=${SheetTreeLog.tag(getChildAt(index))}" +
" children=${SheetTreeLog.childTags(this)}",
)
if (getChildAt(index) === reactView) releaseReactView()
super.removeViewAt(index)
}
Expand Down
11 changes: 10 additions & 1 deletion android/src/main/java/com/sheet2/FragmentModalBottomSheet.kt
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,16 @@ class FragmentModalBottomSheet() : BottomSheetDialogFragment() {
val view = overlayView ?: return
val decorView = dialog?.window?.decorView as? ViewGroup ?: return
detachOverlayHost()
(view.parent as? ViewGroup)?.removeView(view)
// Same as the inline presenter: the overlay is taken out of whatever parent
// React Native gave it and put into the dialog's own host.
val previousParent = view.parent as? ViewGroup
SheetTreeLog.log(
view.context,
"dialog.attachOverlay",
"overlay=${SheetTreeLog.tag(view)} from=${SheetTreeLog.tag(previousParent)}" +
" fromChildren=${SheetTreeLog.childTags(previousParent)}",
)
previousParent?.removeView(view)

val host = DialogPassThroughFrameLayout(decorView.context).apply {
layoutParams = ViewGroup.LayoutParams(
Expand Down
11 changes: 10 additions & 1 deletion android/src/main/java/com/sheet2/InlineSheetPresenter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,16 @@ internal class InlineSheetPresenter(
private fun attachOverlayView() {
val host = overlayHost ?: return
val view = overlayView ?: return
(view.parent as? ViewGroup)?.removeView(view)
// Re-parenting a view React Native mounted somewhere else: its old parent
// loses a child without React Native being told.
val previousParent = view.parent as? ViewGroup
SheetTreeLog.log(
view.context,
"inline.attachOverlay",
"overlay=${SheetTreeLog.tag(view)} from=${SheetTreeLog.tag(previousParent)}" +
" fromChildren=${SheetTreeLog.childTags(previousParent)} to=${SheetTreeLog.tag(host)}",
)
previousParent?.removeView(view)
view.layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT,
Expand Down
7 changes: 7 additions & 0 deletions android/src/main/java/com/sheet2/SheetModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.WritableArray
import com.facebook.react.bridge.WritableMap

class SheetModule(reactContext: ReactApplicationContext) : NativeSheetSpec(reactContext) {
Expand All @@ -23,6 +24,12 @@ class SheetModule(reactContext: ReactApplicationContext) : NativeSheetSpec(react
return Arguments.createMap()
}

override fun getTreeLog(): WritableArray {
val entries = Arguments.createArray()
SheetTreeLog.snapshot().forEach { entries.pushString(it) }
return entries
}

override fun dismissAll() {
reactApplicationContext.runOnUiQueueThread {
reactApplicationContext.currentActivity?.let { AppFittedSheet.Companion.dismissAll(it as AppCompatActivity) }
Expand Down
71 changes: 71 additions & 0 deletions android/src/main/java/com/sheet2/SheetTreeLog.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.sheet2

import android.content.Context
import android.util.Log
import android.view.View
import android.view.ViewGroup
import com.facebook.react.bridge.ReactContext
import com.facebook.react.modules.core.DeviceEventManagerModule

/**
* Every place where this library moves a view React Native believes it owns.
*
* The mounting crashes worth chasing carry no frame of ours: on Android the
* stack between `ViewGroupManager.addView` and `ViewGroup.addInArray` is React
* Native and the framework only ("addViewAt: failed to insert view [child] into
* parent [parent] at index N", caused by "IndexOutOfBoundsException: index=N
* count=0"). So the only way to tell afterwards whether a sheet was involved is
* to say so ourselves, naming the views by the id React Native uses as the tag —
* which is exactly what that message carries.
*
* Entries go to logcat, to a small in-memory ring for a local session, and — when
* a listener is registered from JS — to the app, which is expected to turn them
* into breadcrumbs. Breadcrumbs are what survives into a native crash report.
*/
object SheetTreeLog {
private const val TAG = "Sheet2Tree"
private const val EVENT = "sheet2:tree"
private const val CAPACITY = 32

private val entries = ArrayDeque<String>()

fun log(context: Context?, op: String, details: String) {
val entry = "$op $details"

synchronized(entries) {
if (entries.size == CAPACITY) entries.removeFirst()
entries.addLast(entry)
}

Log.d(TAG, entry)
emit(context, entry)
}

/** The tag React Native knows the view by, or "-" when there is no view. */
fun tag(view: View?): String = view?.let { "${it.id}" } ?: "-"

fun childTags(parent: ViewGroup?): String {
if (parent == null) return "-"
val count = parent.childCount
if (count == 0) return "none"

return (0 until count).joinToString(",") { "${parent.getChildAt(it).id}" }
}

fun snapshot(): List<String> = synchronized(entries) { entries.toList() }

private fun emit(context: Context?, entry: String) {
val reactContext = context as? ReactContext ?: return
if (!reactContext.hasActiveReactInstance()) return

try {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit(EVENT, entry)
} catch (error: Throwable) {
// A sheet can be torn down while the instance is going away; diagnostics
// must never be the reason something crashes.
Log.d(TAG, "emit failed: ${error.message}")
}
}
}
23 changes: 23 additions & 0 deletions ios/HostFittedSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,14 @@ public final class HostFittedSheet: UIView {
}

public override func insertReactSubview(_ subview: UIView!, at atIndex: Int) {
SheetTreeLog.log(
"sheet.insertReactSubview",
details: "child=\(SheetTreeLog.name(subview)) index=\(atIndex) self=\(SheetTreeLog.name(self))"
+ " presented=\(_isPresented)"
)
if atIndex > 0 {
// The overlay never becomes a subview of the content view React Native
// mounted it into.
_overlaySubview = subview
subview.isUserInteractionEnabled = false
attachOverlaySubview()
Expand All @@ -148,6 +155,11 @@ public final class HostFittedSheet: UIView {
}

public override func removeReactSubview(_ subview: UIView!) {
SheetTreeLog.log(
"sheet.removeReactSubview",
details: "child=\(SheetTreeLog.name(subview)) self=\(SheetTreeLog.name(self))"
+ " presented=\(_isPresented)"
)
if let overlaySubview = _overlaySubview, subview === overlaySubview {
_overlaySubview?.removeFromSuperview()
_overlaySubview = nil
Expand All @@ -166,6 +178,12 @@ public final class HostFittedSheet: UIView {
guard let overlaySubview = _overlaySubview,
let sheetView = _modalViewController?.view else { return }

SheetTreeLog.log(
"sheet.attachOverlay",
details: "overlay=\(SheetTreeLog.name(overlaySubview))"
+ " from=\(SheetTreeLog.name(overlaySubview.superview))"
+ " to=\(SheetTreeLog.name(sheetView))"
)
overlaySubview.removeFromSuperview()
overlaySubview.frame = sheetView.bounds
overlaySubview.autoresizingMask = [.flexibleWidth, .flexibleHeight]
Expand Down Expand Up @@ -416,6 +434,11 @@ public final class HostFittedSheet: UIView {

@objc
public func destroy() {
SheetTreeLog.log(
"sheet.destroy",
details: "self=\(SheetTreeLog.name(self)) content=\(SheetTreeLog.name(_reactSubview))"
+ " overlay=\(SheetTreeLog.name(_overlaySubview)) presented=\(_isPresented)"
)
if !_isPresented && _modalViewController == nil {
return
}
Expand Down
4 changes: 4 additions & 0 deletions ios/SheetModule.mm
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ - (NSDictionary *)viewportSize {
return dict;
}

- (NSArray<NSString *> *)getTreeLog {
return [SheetTreeLog snapshot];
}

- (void)dismiss:(NSInteger)tag {
NSLog(@"😀 dismissModule %d", [[NSNumber alloc] initWithInt:tag].intValue);
}
Expand Down
49 changes: 49 additions & 0 deletions ios/SheetTreeLog.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import Foundation
import UIKit
import os

/**
Every place where this library moves a view React Native believes it owns —
the iOS half of the Android `SheetTreeLog`.

On iOS the crashes do carry our symbols (`HostFittedSheet.setFittedSheetParams`,
`dismissSheet` inside `RCTMountingManager.performTransaction`), so what is
missing there is not the name but the state: which subview was mounted, which
one was taken away, and whether the sheet was already being destroyed. That is
what these entries carry.

The trail lives in memory and in the unified log under the `com.sheet2`
subsystem; `getTreeLog()` on the Sheet module hands the last entries to JS.
*/
@objc public class SheetTreeLog: NSObject {
private static let capacity = 32
private static let lock = NSLock()
private static var entries: [String] = []
private static let osLog = OSLog(subsystem: "com.sheet2", category: "tree")

@objc public static func log(_ op: String, details: String) {
let entry = "\(op) \(details)"

lock.lock()
entries.append(entry)
if entries.count > capacity {
entries.removeFirst(entries.count - capacity)
}
lock.unlock()

os_log("%{public}@", log: osLog, type: .debug, entry)
}

@objc public static func snapshot() -> [String] {
lock.lock()
defer { lock.unlock() }
return entries
}

/// A view named the way a crash report names it: the React tag when the view
/// carries one, and the address, which is what an EXC_BAD_ACCESS points at.
@objc public static func name(_ view: UIView?) -> String {
guard let view else { return "-" }
return "tag=\(view.tag)/\(Unmanaged.passUnretained(view).toOpaque())"
}
}
12 changes: 12 additions & 0 deletions ios/SheetView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
}

- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index {
[SheetTreeLog log:@"host.mountChild"
details:[NSString stringWithFormat:@"child=%@ index=%ld host=%@",
[SheetTreeLog name:childComponentView], (long)index,
[SheetTreeLog name:_view2]]];
[_view2 insertReactSubview:childComponentView atIndex:index];

__weak SheetView *weakSelf = self;
Expand All @@ -150,10 +154,18 @@ - (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childCompone
}

- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index {
[SheetTreeLog log:@"host.unmountChild"
details:[NSString stringWithFormat:@"child=%@ index=%ld host=%@",
[SheetTreeLog name:childComponentView], (long)index,
[SheetTreeLog name:_view2]]];
[_view2 removeReactSubview:childComponentView];
}

- (void)prepareForRecycle {
// The HostFittedSheet is thrown away and rebuilt here, so anything the
// mounting layer still holds from before this point is stale.
[SheetTreeLog log:@"host.prepareForRecycle"
details:[NSString stringWithFormat:@"host=%@", [SheetTreeLog name:_view2]]];
[_view2 destroy];
_view2.onSheetDismiss = nil;

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"example"
],
"private": "true",
"version": "7.10.2",
"version": "7.11.0",
"description": "Native implementation of Bottom sheet",
"main": "src/index.tsx",
"module": "src/index",
Expand Down
Loading
Loading