From 513f23c49f03d30f1278dc89010ecd064c55990b Mon Sep 17 00:00:00 2001 From: Patrick Kabwe Date: Wed, 8 Jul 2026 09:33:14 +0200 Subject: [PATCH] feat: add `measureContent` support for Nitro Views --- docs/docs/guides/view-components.md | 64 ++++++ example/ios/Podfile.lock | 12 +- example/src/screens/ViewScreen.tsx | 44 ++++ .../src/views/CppHybridViewComponent.ts | 61 ++++- .../src/views/createViewMeasurerShared.ts | 60 +++++ .../views/kotlin/KotlinHybridViewManager.ts | 10 + .../views/kotlin/createKotlinViewMeasurer.ts | 216 ++++++++++++++++++ .../src/views/swift/SwiftHybridViewManager.ts | 7 + .../views/swift/createSwiftViewMeasurer.ts | 156 +++++++++++++ .../com/margelo/nitro/views/MeasurableView.kt | 50 ++++ .../ios/views/MeasurableView.swift | 42 ++++ .../margelo/nitro/test/HybridMeasuredView.kt | 63 +++++ .../margelo/nitro/test/NitroTestPackage.kt | 2 + .../ios/HybridMeasuredView.swift | 36 +++ packages/react-native-nitro-test/nitro.json | 10 + .../android/NitroTest+autolinking.cmake | 4 + .../generated/android/NitroTestOnLoad.cpp | 18 ++ .../android/c++/JHybridMeasuredViewSpec.cpp | 66 ++++++ .../android/c++/JHybridMeasuredViewSpec.hpp | 66 ++++++ .../android/c++/JMeasuredViewProps.hpp | 61 +++++ .../android/c++/JRecyclableTestViewProps.hpp | 57 +++++ .../generated/android/c++/JTestViewProps.hpp | 66 ++++++ .../c++/views/JHybridMeasuredViewMeasurer.hpp | 75 ++++++ .../views/JHybridMeasuredViewStateUpdater.cpp | 60 +++++ .../views/JHybridMeasuredViewStateUpdater.hpp | 52 +++++ .../JHybridRecyclableTestViewMeasurer.hpp | 75 ++++++ .../JHybridRecyclableTestViewStateUpdater.hpp | 3 + .../c++/views/JHybridTestViewMeasurer.hpp | 75 ++++++ .../c++/views/JHybridTestViewStateUpdater.hpp | 3 + .../nitro/test/HybridMeasuredViewSpec.kt | 63 +++++ .../margelo/nitro/test/MeasuredViewProps.kt | 56 +++++ .../nitro/test/RecyclableTestViewProps.kt | 51 +++++ .../com/margelo/nitro/test/TestViewProps.kt | 61 +++++ .../test/views/HybridMeasuredViewManager.kt | 80 +++++++ .../test/views/HybridMeasuredViewMeasurer.kt | 66 ++++++ .../views/HybridMeasuredViewStateUpdater.kt | 23 ++ .../views/HybridRecyclableTestViewMeasurer.kt | 66 ++++++ .../test/views/HybridTestViewMeasurer.kt | 66 ++++++ .../ios/NitroTest-Swift-Cxx-Bridge.cpp | 17 ++ .../ios/NitroTest-Swift-Cxx-Bridge.hpp | 17 ++ .../ios/NitroTest-Swift-Cxx-Umbrella.hpp | 23 ++ .../generated/ios/NitroTestAutolinking.mm | 8 + .../generated/ios/NitroTestAutolinking.swift | 12 + .../ios/c++/HybridMeasuredViewSpecSwift.cpp | 11 + .../ios/c++/HybridMeasuredViewSpecSwift.hpp | 87 +++++++ .../c++/views/HybridMeasuredViewComponent.mm | 152 ++++++++++++ .../HybridRecyclableTestViewComponent.mm | 24 ++ .../ios/c++/views/HybridTestViewComponent.mm | 26 +++ .../ios/swift/HybridMeasuredViewSpec.swift | 56 +++++ .../swift/HybridMeasuredViewSpec_cxx.swift | 167 ++++++++++++++ .../ios/swift/LayoutConstraints.swift | 34 +++ .../generated/ios/swift/LayoutContext.swift | 34 +++ .../ios/swift/MeasuredViewProps.swift | 34 +++ .../ios/swift/RecyclableTestViewProps.swift | 29 +++ .../nitrogen/generated/ios/swift/Size.swift | 34 +++ .../generated/ios/swift/TestViewProps.swift | 39 ++++ .../views/HybridMeasuredViewMeasurer.swift | 44 ++++ .../HybridRecyclableTestViewMeasurer.swift | 44 ++++ .../swift/views/HybridTestViewMeasurer.swift | 44 ++++ .../shared/c++/HybridMeasuredViewSpec.cpp | 24 ++ .../shared/c++/HybridMeasuredViewSpec.hpp | 65 ++++++ .../shared/c++/LayoutConstraints.hpp | 88 +++++++ .../generated/shared/c++/LayoutContext.hpp | 87 +++++++ .../shared/c++/MeasuredViewProps.hpp | 87 +++++++ .../shared/c++/RecyclableTestViewProps.hpp | 83 +++++++ .../nitrogen/generated/shared/c++/Size.hpp | 87 +++++++ .../generated/shared/c++/TestViewProps.hpp | 92 ++++++++ .../c++/views/HybridMeasuredViewComponent.cpp | 94 ++++++++ .../c++/views/HybridMeasuredViewComponent.hpp | 165 +++++++++++++ .../HybridRecyclableTestViewComponent.hpp | 61 ++++- .../c++/views/HybridTestViewComponent.hpp | 61 ++++- .../shared/json/MeasuredViewConfig.json | 11 + packages/react-native-nitro-test/src/index.ts | 2 + .../src/specs/MeasuredView.nitro.ts | 18 ++ .../src/views/MeasuredView.ts | 13 ++ 75 files changed, 4002 insertions(+), 18 deletions(-) create mode 100644 packages/nitrogen/src/views/createViewMeasurerShared.ts create mode 100644 packages/nitrogen/src/views/kotlin/createKotlinViewMeasurer.ts create mode 100644 packages/nitrogen/src/views/swift/createSwiftViewMeasurer.ts create mode 100644 packages/react-native-nitro-modules/android/src/main/java/com/margelo/nitro/views/MeasurableView.kt create mode 100644 packages/react-native-nitro-modules/ios/views/MeasurableView.swift create mode 100644 packages/react-native-nitro-test/android/src/main/java/com/margelo/nitro/test/HybridMeasuredView.kt create mode 100644 packages/react-native-nitro-test/ios/HybridMeasuredView.swift create mode 100644 packages/react-native-nitro-test/nitrogen/generated/android/c++/JHybridMeasuredViewSpec.cpp create mode 100644 packages/react-native-nitro-test/nitrogen/generated/android/c++/JHybridMeasuredViewSpec.hpp create mode 100644 packages/react-native-nitro-test/nitrogen/generated/android/c++/JMeasuredViewProps.hpp create mode 100644 packages/react-native-nitro-test/nitrogen/generated/android/c++/JRecyclableTestViewProps.hpp create mode 100644 packages/react-native-nitro-test/nitrogen/generated/android/c++/JTestViewProps.hpp create mode 100644 packages/react-native-nitro-test/nitrogen/generated/android/c++/views/JHybridMeasuredViewMeasurer.hpp create mode 100644 packages/react-native-nitro-test/nitrogen/generated/android/c++/views/JHybridMeasuredViewStateUpdater.cpp create mode 100644 packages/react-native-nitro-test/nitrogen/generated/android/c++/views/JHybridMeasuredViewStateUpdater.hpp create mode 100644 packages/react-native-nitro-test/nitrogen/generated/android/c++/views/JHybridRecyclableTestViewMeasurer.hpp create mode 100644 packages/react-native-nitro-test/nitrogen/generated/android/c++/views/JHybridTestViewMeasurer.hpp create mode 100644 packages/react-native-nitro-test/nitrogen/generated/android/kotlin/com/margelo/nitro/test/HybridMeasuredViewSpec.kt create mode 100644 packages/react-native-nitro-test/nitrogen/generated/android/kotlin/com/margelo/nitro/test/MeasuredViewProps.kt create mode 100644 packages/react-native-nitro-test/nitrogen/generated/android/kotlin/com/margelo/nitro/test/RecyclableTestViewProps.kt create mode 100644 packages/react-native-nitro-test/nitrogen/generated/android/kotlin/com/margelo/nitro/test/TestViewProps.kt create mode 100644 packages/react-native-nitro-test/nitrogen/generated/android/kotlin/com/margelo/nitro/test/views/HybridMeasuredViewManager.kt create mode 100644 packages/react-native-nitro-test/nitrogen/generated/android/kotlin/com/margelo/nitro/test/views/HybridMeasuredViewMeasurer.kt create mode 100644 packages/react-native-nitro-test/nitrogen/generated/android/kotlin/com/margelo/nitro/test/views/HybridMeasuredViewStateUpdater.kt create mode 100644 packages/react-native-nitro-test/nitrogen/generated/android/kotlin/com/margelo/nitro/test/views/HybridRecyclableTestViewMeasurer.kt create mode 100644 packages/react-native-nitro-test/nitrogen/generated/android/kotlin/com/margelo/nitro/test/views/HybridTestViewMeasurer.kt create mode 100644 packages/react-native-nitro-test/nitrogen/generated/ios/c++/HybridMeasuredViewSpecSwift.cpp create mode 100644 packages/react-native-nitro-test/nitrogen/generated/ios/c++/HybridMeasuredViewSpecSwift.hpp create mode 100644 packages/react-native-nitro-test/nitrogen/generated/ios/c++/views/HybridMeasuredViewComponent.mm create mode 100644 packages/react-native-nitro-test/nitrogen/generated/ios/swift/HybridMeasuredViewSpec.swift create mode 100644 packages/react-native-nitro-test/nitrogen/generated/ios/swift/HybridMeasuredViewSpec_cxx.swift create mode 100644 packages/react-native-nitro-test/nitrogen/generated/ios/swift/LayoutConstraints.swift create mode 100644 packages/react-native-nitro-test/nitrogen/generated/ios/swift/LayoutContext.swift create mode 100644 packages/react-native-nitro-test/nitrogen/generated/ios/swift/MeasuredViewProps.swift create mode 100644 packages/react-native-nitro-test/nitrogen/generated/ios/swift/RecyclableTestViewProps.swift create mode 100644 packages/react-native-nitro-test/nitrogen/generated/ios/swift/Size.swift create mode 100644 packages/react-native-nitro-test/nitrogen/generated/ios/swift/TestViewProps.swift create mode 100644 packages/react-native-nitro-test/nitrogen/generated/ios/swift/views/HybridMeasuredViewMeasurer.swift create mode 100644 packages/react-native-nitro-test/nitrogen/generated/ios/swift/views/HybridRecyclableTestViewMeasurer.swift create mode 100644 packages/react-native-nitro-test/nitrogen/generated/ios/swift/views/HybridTestViewMeasurer.swift create mode 100644 packages/react-native-nitro-test/nitrogen/generated/shared/c++/HybridMeasuredViewSpec.cpp create mode 100644 packages/react-native-nitro-test/nitrogen/generated/shared/c++/HybridMeasuredViewSpec.hpp create mode 100644 packages/react-native-nitro-test/nitrogen/generated/shared/c++/LayoutConstraints.hpp create mode 100644 packages/react-native-nitro-test/nitrogen/generated/shared/c++/LayoutContext.hpp create mode 100644 packages/react-native-nitro-test/nitrogen/generated/shared/c++/MeasuredViewProps.hpp create mode 100644 packages/react-native-nitro-test/nitrogen/generated/shared/c++/RecyclableTestViewProps.hpp create mode 100644 packages/react-native-nitro-test/nitrogen/generated/shared/c++/Size.hpp create mode 100644 packages/react-native-nitro-test/nitrogen/generated/shared/c++/TestViewProps.hpp create mode 100644 packages/react-native-nitro-test/nitrogen/generated/shared/c++/views/HybridMeasuredViewComponent.cpp create mode 100644 packages/react-native-nitro-test/nitrogen/generated/shared/c++/views/HybridMeasuredViewComponent.hpp create mode 100644 packages/react-native-nitro-test/nitrogen/generated/shared/json/MeasuredViewConfig.json create mode 100644 packages/react-native-nitro-test/src/specs/MeasuredView.nitro.ts create mode 100644 packages/react-native-nitro-test/src/views/MeasuredView.ts diff --git a/docs/docs/guides/view-components.md b/docs/docs/guides/view-components.md index b44f195d8e..ff28b9812f 100644 --- a/docs/docs/guides/view-components.md +++ b/docs/docs/guides/view-components.md @@ -158,6 +158,70 @@ function App() { } ``` +## Self-measurement (`MeasurableView`) + +By default a `HybridView` is sized by its children or by explicit style props. To instead size a view to its **own content** - a `` that grows to fit its string, a chart that derives a height from its data - conform your native implementation to `MeasurableView`. Nitro then makes the view a measurable Yoga **leaf**: Yoga calls your `measureContent` during layout and uses the returned `Size` as the intrinsic size, so no explicit `height` is needed. Like [`RecyclableView`](#recycling) it's a native opt-in, so **your `.nitro.ts` spec is unchanged**: + +```ts title="NitroTextView.nitro.ts" +export interface NitroTextViewProps extends HybridViewProps { + text: string + fontSize: number +} +export type NitroTextView = HybridView +``` + + + + ```swift title="HybridNitroTextView.swift" + class HybridNitroTextView: HybridNitroTextViewSpec, MeasurableView { + let view = UILabel() + var text: String = "" { didSet { view.text = text } } + var fontSize: Double = 17 { didSet { view.font = .systemFont(ofSize: fontSize) } } + + // `static` - no view instance needed. + // highlight-next-line + static func measureContent(props: NitroTextViewProps, layoutContext: LayoutContext, layoutConstraints: LayoutConstraints) -> Size { + // Measure `props.text` within `layoutConstraints.maximumSize` (off-thread, e.g. NSAttributedString.boundingRect). + return Size(width: measuredWidth, height: measuredHeight) + } + } + ``` + + + ```kotlin title="HybridNitroTextView.kt" + class HybridNitroTextView(context: Context) : HybridNitroTextViewSpec() { + override val view = TextView(context) + override var text: String = "" /* set { view.text = … } */ + override var fontSize: Double = 17.0 /* set { view.textSize = … } */ + + // Kotlin interfaces can't require statics, so measureContent lives on the companion. + companion object : MeasurableView { + override fun measureContent(props: NitroTextViewProps, layoutContext: LayoutContext, layoutConstraints: LayoutConstraints): Size { + // Measure `props.text` within `layoutConstraints.maximumSize` (off-thread, e.g. StaticLayout). + return Size(measuredWidth, measuredHeight) + } + } + } + ``` + + + +Now `` grows to fit its text - no `height` in its style: + +```jsx + +``` + +Because Yoga runs `measureContent` on the shadow thread during layout, it has a few hard rules: + +- **No view, pure function.** Never touch your native view inside it (it may not exist yet, and runs on another thread). Compute the size from `props` only - Yoga caches the result keyed on `props` + constraints. Use whatever measurement fits your content and is safe off the main thread (text → `boundingRect`/`StaticLayout`, an image → its source dimensions, …). +- **It's a Yoga leaf**, so a measurable view **can't also host React children** - the two are mutually exclusive (like RN's ``). +- `maximumSize.width`/`.height` is `.infinity` on an unconstrained axis (return your intrinsic size) and finite when bounded (measure within it, e.g. to wrap text). + +:::caution Not every view can self-measure +The size must be derivable from `props`, off the main thread, with no mounted view. Content that can only be sized on the main thread or via a live view's layout pass (UIKit `systemLayoutSizeFitting`, Android `View.measure()`) doesn't fit - use flexbox + explicit sizing, or precompute the size and pass it in as props. +::: + ## Props Since every `HybridView` is also a `HybridObject`, you can use any type that Nitro supports as a property - including custom types (`interface`), `ArrayBuffer`, and even other `HybridObject`s! diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index a010eedacb..22496ca21b 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -2227,10 +2227,10 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FBLazyVector: 24e62c765683b8d89006a88a2c8f5cf019f0074d - hermes-engine: f0e8ed549bb5a3befb3e292e5c0dafd3e2cee3ab - NitroModules: 16bc17a076b12304d608f7c915b9d321f56dfc19 - NitroTest: 015ec3bddf31c0bd83c0d6d4e10b6d3a42e4e18d - NitroTestExternal: 01bc29bd779142e98432f2178760baebb134de57 + hermes-engine: 0bf3e46f12b9a9102be444d5d30dd35eebd0324e + NitroModules: 1c12300c03d751a09723430c05a420667ffde90f + NitroTest: 767bbefb4d84ac67849f5545f1a96d21caf5deeb + NitroTestExternal: 2808c429cfc1724e2695a15a2eb13c8008cc8b51 RCTDeprecation: a4c521821fab57cbb125b36effe84d897d0dfa12 RCTRequired: 9f3a7e5645d4bc3f551593de7550bb66ab6e42bc RCTSwiftUI: 239ed2eb9e73de5a6f518810630f0c95e01c8702 @@ -2239,7 +2239,7 @@ SPEC CHECKSUMS: React: e2dc35338068bbd299c66f043ae0d7f25de8499e React-callinvoker: 28b25d21b124c26cebaea713ba7d801b9351dc48 React-Core: 02ed7d2ffb70437bdf2aba074a13078a7b0b9ff0 - React-Core-prebuilt: 72454b4caa0309d15c45de529b1c5a0e4607cf9a + React-Core-prebuilt: fb974d1664c98b4ece04886442bbad2fa51c80eb React-CoreModules: b3a5a42dadcde3b5d47b325bd912eb2ced89e146 React-cxxreact: fe8f88dda044e5905e99a00f41b7a874c3908716 React-debug: 92944dc4d89f56d640e75498266cbde557a48189 @@ -2303,7 +2303,7 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: 25c9c516839be2c5e3d3344f95dc7da5f7e63fc2 ReactCodegen: 0f100aa6334186385a43f0dd13d63efc6805ea55 ReactCommon: 7dfc3250793bf36cf221096ff59e1179e13eef7f - ReactNativeDependencies: 86c5427d73b954c0671c6ab9691d486b196595b6 + ReactNativeDependencies: 589c57612aa76b4cf9130967abc61b82999676ef RNScreens: 991cc417cd396602a6cf59a42139e5a9d91462a9 Yoga: 77dfa8673de2874e1855002ae59c68b8be9b007b diff --git a/example/src/screens/ViewScreen.tsx b/example/src/screens/ViewScreen.tsx index c17b84864d..e23d4f04aa 100644 --- a/example/src/screens/ViewScreen.tsx +++ b/example/src/screens/ViewScreen.tsx @@ -5,6 +5,7 @@ import { callback, NitroModules } from 'react-native-nitro-modules' import { useSafeAreaInsets } from 'react-native-safe-area-context' import { useColors } from '../useColors' import { + MeasuredView, HybridTestObjectSwiftKotlin, RecyclableTestView, TestView, @@ -14,6 +15,30 @@ import { useIsFocused } from '@react-navigation/native' const VIEWS_X = 15 const VIEWS_Y = 15 + +const MeasuredViewImpl = () => { + const [longText, setLongText] = React.useState(false) + const measuredText = longText + ? 'MeasuredView sizes its own height from this text a long string that wraps onto several lines, so the bordered box grows to fit it. Tap the button to shrink it.' + : 'MeasuredView: tap below to grow.' + + return ( + + + +