@momo-kits/arrangement-view 0.0.0-stage → 0.167.1-beta.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,30 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "ArrangementView"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => "15.1" }
14
+ s.swift_version = "5.0"
15
+ s.frameworks = "SwiftUI"
16
+
17
+ # Runtime availability alone cannot compile symbols absent from an older SDK.
18
+ sdk_version = `xcrun --sdk iphoneos --show-sdk-version`.strip
19
+ has_arrangement = Gem::Version.new(sdk_version) >= Gem::Version.new("27.1")
20
+ s.pod_target_xcconfig = {
21
+ "DEFINES_MODULE" => "YES",
22
+ "SWIFT_ACTIVE_COMPILATION_CONDITIONS" => "$(inherited) #{has_arrangement ? 'RAV_HAS_ARRANGEMENT' : ''}"
23
+ }
24
+ s.source = { :git => "https://www.npmjs.com/.git", :tag => "#{s.version}" }
25
+
26
+ s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
27
+ s.private_header_files = "ios/**/*.h"
28
+
29
+ install_modules_dependencies(s)
30
+ end
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Thiago Brezinski
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
@@ -0,0 +1,64 @@
1
+ buildscript {
2
+ ext.ArrangementView = [
3
+ kotlinVersion: "2.1.20",
4
+ minSdkVersion: 24,
5
+ compileSdkVersion: 36,
6
+ targetSdkVersion: 36
7
+ ]
8
+
9
+ ext.getExtOrDefault = { prop ->
10
+ if (rootProject.ext.has(prop)) {
11
+ return rootProject.ext.get(prop)
12
+ }
13
+
14
+ return ArrangementView[prop]
15
+ }
16
+
17
+ repositories {
18
+ google()
19
+ mavenCentral()
20
+ }
21
+
22
+ dependencies {
23
+ classpath "com.android.tools.build:gradle:8.11.0"
24
+ // noinspection DifferentKotlinGradleVersion
25
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
26
+ }
27
+ }
28
+
29
+ apply plugin: "com.android.library"
30
+ apply plugin: "kotlin-android"
31
+ apply plugin: "com.facebook.react"
32
+
33
+ android {
34
+ namespace "com.arrangementview"
35
+
36
+ compileSdkVersion getExtOrDefault("compileSdkVersion")
37
+
38
+ defaultConfig {
39
+ minSdkVersion getExtOrDefault("minSdkVersion")
40
+ targetSdkVersion getExtOrDefault("targetSdkVersion")
41
+ }
42
+
43
+ buildTypes {
44
+ release {
45
+ minifyEnabled false
46
+ }
47
+ }
48
+
49
+ lint {
50
+ disable "GradleCompatible"
51
+ }
52
+
53
+ compileOptions {
54
+ sourceCompatibility JavaVersion.VERSION_1_8
55
+ targetCompatibility JavaVersion.VERSION_1_8
56
+ }
57
+ }
58
+
59
+ dependencies {
60
+ implementation "com.facebook.react:react-android"
61
+ // Jetpack WindowManager reports the fold; window-java adapts its Flow to a callback.
62
+ implementation "androidx.window:window:1.5.1"
63
+ implementation "androidx.window:window-java:1.5.1"
64
+ }
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -0,0 +1,242 @@
1
+ package com.arrangementview
2
+
3
+ import android.content.Context
4
+ import android.graphics.Rect
5
+ import android.hardware.Sensor
6
+ import android.hardware.SensorEvent
7
+ import android.hardware.SensorEventListener
8
+ import android.hardware.SensorManager
9
+ import android.os.Build
10
+ import android.view.View
11
+ import android.view.ViewTreeObserver
12
+ import androidx.core.content.ContextCompat
13
+ import androidx.core.util.Consumer
14
+ import androidx.window.java.layout.WindowInfoTrackerCallbackAdapter
15
+ import androidx.window.layout.FoldingFeature
16
+ import androidx.window.layout.WindowInfoTracker
17
+ import androidx.window.layout.WindowLayoutInfo
18
+ import com.facebook.react.bridge.Arguments
19
+ import com.facebook.react.bridge.WritableMap
20
+ import com.facebook.react.uimanager.PixelUtil
21
+ import com.facebook.react.uimanager.ThemedReactContext
22
+ import com.facebook.react.uimanager.UIManagerHelper
23
+ import com.facebook.react.uimanager.events.Event
24
+ import com.facebook.react.views.view.ReactViewGroup
25
+
26
+ /**
27
+ * Android has no system arrangement container, so this view only reports what the layout policy
28
+ * in src/arrange.ts needs: its size, its window's size, the fold crossing it, and the hinge. React
29
+ * lays out the panes.
30
+ */
31
+ class ArrangementView(private val reactContext: ThemedReactContext) :
32
+ ReactViewGroup(reactContext), SensorEventListener {
33
+
34
+ var observeHinge = true
35
+ set(value) {
36
+ if (field == value) return
37
+ field = value
38
+ updateHingeSensor()
39
+ // JS resets to unavailable when disabled; re-enabling must replay the current state.
40
+ lastHinge = null
41
+ emitHinge()
42
+ }
43
+
44
+ private val windowInfo = WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(reactContext))
45
+ private val sensorManager = reactContext.getSystemService(Context.SENSOR_SERVICE) as SensorManager
46
+ private val hingeSensor: Sensor? =
47
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
48
+ sensorManager.getDefaultSensor(Sensor.TYPE_HINGE_ANGLE)
49
+ } else {
50
+ null
51
+ }
52
+
53
+ private var fold: FoldingFeature? = null
54
+ private var hingeDegrees: Float? = null
55
+ private var lastHinge: Triple<Boolean, Double, String>? = null
56
+
57
+ /**
58
+ * Yoga's padding and border for this view. Fabric does not apply them to the Android view,
59
+ * because Yoga has already positioned the descendants. JS lays the panes out inside a wrapper
60
+ * that fills the content box, so geometry is reported in that box.
61
+ */
62
+ val contentInsets = Rect()
63
+
64
+ fun setContentInsets(left: Int, top: Int, right: Int, bottom: Int) {
65
+ contentInsets.set(left, top, right, bottom)
66
+ // Moving padding from one edge to another keeps the size and moves the fold.
67
+ emitGeometry()
68
+ }
69
+
70
+ // Geometry is withheld until the first WindowLayoutInfo so JS never arranges without the fold.
71
+ private var hasWindowLayout = false
72
+ private val foldInView = Rect()
73
+ private var lastGeometry: List<Any?>? = null
74
+
75
+ private val onWindowLayout = Consumer<WindowLayoutInfo> { info ->
76
+ fold = info.displayFeatures.filterIsInstance<FoldingFeature>().firstOrNull()
77
+ hasWindowLayout = true
78
+ emitGeometry()
79
+ emitHinge()
80
+ }
81
+
82
+ // Fabric positions views without a layout pass, so ancestors can move this view without any
83
+ // callback reaching it. Checking before each draw is cheap and sees every such change.
84
+ private val onPreDraw = ViewTreeObserver.OnPreDrawListener {
85
+ emitGeometry()
86
+ true
87
+ }
88
+
89
+ override fun onAttachedToWindow() {
90
+ super.onAttachedToWindow()
91
+ val activity = reactContext.currentActivity
92
+ if (activity != null) {
93
+ windowInfo.addWindowLayoutInfoListener(
94
+ activity, ContextCompat.getMainExecutor(reactContext), onWindowLayout)
95
+ } else {
96
+ hasWindowLayout = true
97
+ }
98
+ viewTreeObserver.addOnPreDrawListener(onPreDraw)
99
+ updateHingeSensor()
100
+ }
101
+
102
+ override fun onDetachedFromWindow() {
103
+ super.onDetachedFromWindow()
104
+ windowInfo.removeWindowLayoutInfoListener(onWindowLayout)
105
+ viewTreeObserver.removeOnPreDrawListener(onPreDraw)
106
+ sensorManager.unregisterListener(this)
107
+ }
108
+
109
+ private fun updateHingeSensor() {
110
+ sensorManager.unregisterListener(this)
111
+ if (observeHinge && isAttachedToWindow && hingeSensor != null) {
112
+ sensorManager.registerListener(this, hingeSensor, SensorManager.SENSOR_DELAY_NORMAL)
113
+ }
114
+ }
115
+
116
+ override fun onSensorChanged(event: SensorEvent) {
117
+ hingeDegrees = event.values[0]
118
+ emitHinge()
119
+ }
120
+
121
+ override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) = Unit
122
+
123
+ private fun emitHinge() {
124
+ if (!observeHinge) return
125
+ val degrees = hingeDegrees
126
+ // WindowManager names the open postures. It reports no fold for a closed device, whose app
127
+ // runs on the cover display, so only there does the angle decide.
128
+ val status =
129
+ when {
130
+ fold?.state == FoldingFeature.State.FLAT -> "fullyOpen"
131
+ fold?.state == FoldingFeature.State.HALF_OPENED -> "partiallyOpen"
132
+ degrees != null && degrees < CLOSED_DEGREES -> "closed"
133
+ else -> "unknown"
134
+ }
135
+ val available = fold != null || hingeSensor != null
136
+ val radians = if (degrees != null) Math.toRadians(degrees.toDouble()) else Double.NaN
137
+ val hinge = Triple(available, radians, status)
138
+ if (hinge == lastHinge) return
139
+ lastHinge = hinge
140
+ dispatch(
141
+ HINGE_EVENT,
142
+ Arguments.createMap().apply {
143
+ putBoolean("available", available)
144
+ putDouble("angle", radians)
145
+ putString("status", status)
146
+ })
147
+ }
148
+
149
+ private fun emitGeometry() {
150
+ if (!hasWindowLayout) return
151
+ val fold = fold
152
+ if (fold != null) {
153
+ // Fold bounds are in window coordinates. Use this view's layout position rather than
154
+ // getLocationInWindow so a screen transition's transform doesn't drag the fold along.
155
+ var x = 0
156
+ var y = 0
157
+ var view: View = this
158
+ while (true) {
159
+ x += view.left
160
+ y += view.top
161
+ val parent = view.parent as? View ?: break
162
+ x -= parent.scrollX
163
+ y -= parent.scrollY
164
+ view = parent
165
+ }
166
+ foldInView.set(fold.bounds)
167
+ foldInView.offset(-x - contentInsets.left, -y - contentInsets.top)
168
+ } else {
169
+ foldInView.setEmpty()
170
+ }
171
+ val width = width - contentInsets.left - contentInsets.right
172
+ val height = height - contentInsets.top - contentInsets.bottom
173
+ // Material sizes panes by the window, as iOS size classes are by the scene.
174
+ val window = rootView
175
+ val geometry =
176
+ listOf(
177
+ width,
178
+ height,
179
+ window.width,
180
+ window.height,
181
+ Rect(foldInView),
182
+ fold?.orientation,
183
+ fold?.isSeparating,
184
+ fold?.state)
185
+ if (geometry == lastGeometry) return
186
+ lastGeometry = geometry
187
+ dispatch(
188
+ GEOMETRY_EVENT,
189
+ Arguments.createMap().apply {
190
+ putDouble("width", dp(width))
191
+ putDouble("height", dp(height))
192
+ putMap(
193
+ "window",
194
+ Arguments.createMap().apply {
195
+ putDouble("width", dp(window.width))
196
+ putDouble("height", dp(window.height))
197
+ })
198
+ if (fold != null) {
199
+ putMap(
200
+ "fold",
201
+ Arguments.createMap().apply {
202
+ putDouble("x", dp(foldInView.left))
203
+ putDouble("y", dp(foldInView.top))
204
+ putDouble("width", dp(foldInView.width()))
205
+ putDouble("height", dp(foldInView.height()))
206
+ putString(
207
+ "orientation",
208
+ if (fold.orientation == FoldingFeature.Orientation.VERTICAL) "vertical"
209
+ else "horizontal")
210
+ putBoolean("separating", fold.isSeparating)
211
+ putBoolean("halfOpened", fold.state == FoldingFeature.State.HALF_OPENED)
212
+ })
213
+ }
214
+ })
215
+ }
216
+
217
+ private fun dp(pixels: Int): Double = PixelUtil.toDIPFromPixel(pixels.toFloat()).toDouble()
218
+
219
+ // getEventDispatcher(context), its replacement, does not exist in React Native 0.83.
220
+ @Suppress("DEPRECATION")
221
+ private fun dispatch(name: String, payload: WritableMap) {
222
+ UIManagerHelper.getEventDispatcherForReactTag(reactContext, id)
223
+ ?.dispatchEvent(ArrangementEvent(UIManagerHelper.getSurfaceId(this), id, name, payload))
224
+ }
225
+
226
+ private class ArrangementEvent(
227
+ surfaceId: Int,
228
+ viewId: Int,
229
+ private val name: String,
230
+ private val payload: WritableMap,
231
+ ) : Event<ArrangementEvent>(surfaceId, viewId) {
232
+ override fun getEventName(): String = name
233
+
234
+ override fun getEventData(): WritableMap = payload
235
+ }
236
+
237
+ companion object {
238
+ const val HINGE_EVENT = "topHingeChange"
239
+ const val GEOMETRY_EVENT = "topGeometryChange"
240
+ private const val CLOSED_DEGREES = 5f
241
+ }
242
+ }
@@ -0,0 +1,36 @@
1
+ package com.arrangementview
2
+
3
+ import com.facebook.react.module.annotations.ReactModule
4
+ import com.facebook.react.uimanager.ThemedReactContext
5
+ import com.facebook.react.uimanager.annotations.ReactProp
6
+ import com.facebook.react.views.view.ReactViewGroup
7
+ import com.facebook.react.views.view.ReactViewManager
8
+
9
+ // Extends the plain View manager so every View style (borders, overflow, ...) keeps working.
10
+ // `arrangement` and `axes` are deliberately not handled here: JS arranges the panes on Android.
11
+ @ReactModule(name = ArrangementViewManager.NAME)
12
+ class ArrangementViewManager : ReactViewManager() {
13
+ override fun getName(): String = NAME
14
+
15
+ override fun createViewInstance(context: ThemedReactContext): ReactViewGroup =
16
+ ArrangementView(context)
17
+
18
+ @ReactProp(name = "observeHinge", defaultBoolean = true)
19
+ fun setObserveHinge(view: ArrangementView, value: Boolean) {
20
+ view.observeHinge = value
21
+ }
22
+
23
+ override fun setPadding(view: ReactViewGroup, left: Int, top: Int, right: Int, bottom: Int) {
24
+ (view as ArrangementView).setContentInsets(left, top, right, bottom)
25
+ }
26
+
27
+ override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> =
28
+ (super.getExportedCustomDirectEventTypeConstants() ?: mutableMapOf()).apply {
29
+ put(ArrangementView.HINGE_EVENT, mapOf("registrationName" to "onHingeChange"))
30
+ put(ArrangementView.GEOMETRY_EVENT, mapOf("registrationName" to "onGeometryChange"))
31
+ }
32
+
33
+ companion object {
34
+ const val NAME = "RNArrangementView"
35
+ }
36
+ }
@@ -0,0 +1,17 @@
1
+ package com.arrangementview
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfoProvider
7
+ import com.facebook.react.uimanager.ViewManager
8
+
9
+ class ArrangementViewPackage : BaseReactPackage() {
10
+ override fun createViewManagers(
11
+ reactContext: ReactApplicationContext
12
+ ): List<ViewManager<*, *>> = listOf(ArrangementViewManager())
13
+
14
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? = null
15
+
16
+ override fun getReactModuleInfoProvider() = ReactModuleInfoProvider { emptyMap() }
17
+ }
@@ -0,0 +1,206 @@
1
+ import SwiftUI
2
+ import Combine
3
+
4
+ private final class ArrangementModel: ObservableObject {
5
+ @Published var primary: UIView?
6
+ @Published var secondary: UIView?
7
+ @Published var arrangement = "split"
8
+ @Published var axes = "both"
9
+ @Published var observeHinge = true
10
+ @Published var forceSplit = false
11
+ @Published var primaryRatio: Double = 0
12
+ var hingeChanged: ((Bool, Double, String) -> Void)?
13
+ var paneLayout: ((UIView, CGRect) -> Void)?
14
+ weak var host: UIView?
15
+
16
+ var supportedAxes: Axis.Set {
17
+ switch axes {
18
+ case "horizontal": return .horizontal
19
+ case "vertical": return .vertical
20
+ default: return [.horizontal, .vertical]
21
+ }
22
+ }
23
+ }
24
+
25
+ // UIKit is only the React Native / SwiftUI hosting boundary. All arrangement
26
+ // decisions and hinge observation below use SwiftUI, including the fallback.
27
+ private final class PaneContainer: UIView {
28
+ weak var model: ArrangementModel?
29
+ let pane: UIView
30
+
31
+ init(pane: UIView, model: ArrangementModel) {
32
+ self.pane = pane
33
+ self.model = model
34
+ super.init(frame: .zero)
35
+ clipsToBounds = true
36
+ addSubview(pane)
37
+ }
38
+ required init?(coder: NSCoder) { fatalError("init(coder:) is unavailable") }
39
+ override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
40
+ let target = super.hitTest(point, with: event)
41
+ return target === self ? nil : target
42
+ }
43
+ override func layoutSubviews() {
44
+ super.layoutSubviews()
45
+ guard let model, let host = model.host else { return }
46
+ model.paneLayout?(pane, convert(bounds, to: host))
47
+ }
48
+ }
49
+
50
+ private struct ReactPane: UIViewRepresentable {
51
+ let pane: UIView
52
+ let model: ArrangementModel
53
+ func makeUIView(context: Context) -> PaneContainer { PaneContainer(pane: pane, model: model) }
54
+ func updateUIView(_ view: PaneContainer, context: Context) { view.setNeedsLayout() }
55
+ @available(iOS 16.0, *)
56
+ func sizeThatFits(_ proposal: ProposedViewSize, uiView: PaneContainer, context: Context) -> CGSize? {
57
+ CGSize(width: proposal.width ?? 0, height: proposal.height ?? 0)
58
+ }
59
+ static func dismantleUIView(_ view: PaneContainer, coordinator: ()) {
60
+ // A style change may already have moved the same React view to its new slot.
61
+ if view.pane.superview === view { view.pane.removeFromSuperview() }
62
+ }
63
+ }
64
+
65
+ private struct ArrangementContent: View {
66
+ @ObservedObject var model: ArrangementModel
67
+
68
+ @ViewBuilder private var primary: some View {
69
+ if let pane = model.primary { ReactPane(pane: pane, model: model) }
70
+ }
71
+ @ViewBuilder private var secondary: some View {
72
+ if let pane = model.secondary { ReactPane(pane: pane, model: model) }
73
+ }
74
+
75
+ @ViewBuilder var body: some View {
76
+ if model.forceSplit && model.arrangement == "split" {
77
+ HStack(spacing: 0) {
78
+ primary.frame(maxWidth: .infinity, maxHeight: .infinity)
79
+ secondary.frame(maxWidth: .infinity, maxHeight: .infinity)
80
+ }
81
+ } else {
82
+ adaptive
83
+ }
84
+ }
85
+
86
+ @ViewBuilder private var adaptive: some View {
87
+ #if RAV_HAS_ARRANGEMENT
88
+ if #available(iOS 27.1, *) {
89
+ nativeArrangement
90
+ .onHingeChange(isEnabled: model.observeHinge) { _, context in
91
+ guard let hinge = context.hinge else {
92
+ model.hingeChanged?(false, 0, "unknown")
93
+ return
94
+ }
95
+ let status: String
96
+ switch hinge.status {
97
+ case .closed: status = "closed"
98
+ case .partiallyOpen: status = "partiallyOpen"
99
+ case .fullyOpen: status = "fullyOpen"
100
+ default: status = "unknown"
101
+ }
102
+ model.hingeChanged?(true, hinge.angle.radians, status)
103
+ }
104
+ } else {
105
+ fallback
106
+ }
107
+ #else
108
+ fallback
109
+ #endif
110
+ }
111
+
112
+ #if RAV_HAS_ARRANGEMENT
113
+ @available(iOS 27.1, *)
114
+ @ViewBuilder private var nativeArrangement: some View {
115
+ if model.arrangement == "overlay" {
116
+ ArrangementView { primary } secondary: { secondary }
117
+ .arrangementViewStyle(.overlay.axes(model.supportedAxes))
118
+ } else {
119
+ ArrangementView {
120
+ primary.splitArrangementLayoutRatio(model.primaryRatio > 0 ? CGFloat(model.primaryRatio) : nil)
121
+ } secondary: {
122
+ secondary
123
+ }
124
+ .arrangementViewStyle(.split.axes(model.supportedAxes))
125
+ }
126
+ }
127
+ #endif
128
+
129
+ @ViewBuilder private var fallback: some View {
130
+ if model.arrangement == "overlay" {
131
+ ZStack { secondary; primary }
132
+ } else {
133
+ // Deterministic compact fallback. The secondary React tree stays mounted.
134
+ primary
135
+ }
136
+ }
137
+ }
138
+
139
+ @objc(RNArrangementHost)
140
+ public final class RNArrangementHost: UIView {
141
+ private let model = ArrangementModel()
142
+ private var controller: UIHostingController<ArrangementContent>!
143
+
144
+ @objc public var onHingeChange: ((Bool, Double, String) -> Void)? {
145
+ didSet { model.hingeChanged = onHingeChange }
146
+ }
147
+ @objc public var onPaneLayout: ((UIView, CGRect) -> Void)? {
148
+ didSet { model.paneLayout = onPaneLayout }
149
+ }
150
+
151
+ @objc public override init(frame: CGRect) {
152
+ super.init(frame: frame)
153
+ model.host = self
154
+ controller = UIHostingController(rootView: ArrangementContent(model: model))
155
+ if #available(iOS 16.4, *) { controller.safeAreaRegions = [] }
156
+ controller.view.backgroundColor = .clear
157
+ addSubview(controller.view)
158
+ clipsToBounds = true
159
+ }
160
+ required init?(coder: NSCoder) { fatalError("init(coder:) is unavailable") }
161
+
162
+ @objc public func setPanes(_ primary: UIView?, secondary: UIView?) {
163
+ if model.primary !== primary { model.primary = primary }
164
+ if model.secondary !== secondary { model.secondary = secondary }
165
+ }
166
+ @objc public func configure(
167
+ _ arrangement: String, axes: String, observeHinge: Bool, forceSplit: Bool, primaryRatio: Double
168
+ ) {
169
+ if model.forceSplit != forceSplit { model.forceSplit = forceSplit }
170
+ if model.primaryRatio != primaryRatio { model.primaryRatio = primaryRatio }
171
+ if model.arrangement != arrangement { model.arrangement = arrangement }
172
+ if model.axes != axes { model.axes = axes }
173
+ if model.observeHinge != observeHinge {
174
+ model.observeHinge = observeHinge
175
+ if !observeHinge { model.hingeChanged?(false, 0, "unknown") }
176
+ }
177
+ }
178
+ public override func layoutSubviews() {
179
+ super.layoutSubviews()
180
+ attachController()
181
+ controller.view.frame = bounds
182
+ }
183
+ public override func didMoveToWindow() {
184
+ super.didMoveToWindow()
185
+ if window == nil { detachController() } else { attachController() }
186
+ }
187
+ private func attachController() {
188
+ guard window != nil else { return }
189
+ var responder: UIResponder? = superview
190
+ while let next = responder {
191
+ if let parent = next as? UIViewController {
192
+ guard controller.parent !== parent else { return }
193
+ detachController()
194
+ parent.addChild(controller)
195
+ controller.didMove(toParent: parent)
196
+ return
197
+ }
198
+ responder = next.next
199
+ }
200
+ }
201
+ private func detachController() {
202
+ guard controller.parent != nil else { return }
203
+ controller.willMove(toParent: nil)
204
+ controller.removeFromParent()
205
+ }
206
+ }
@@ -0,0 +1,5 @@
1
+ #import <React/RCTViewComponentView.h>
2
+
3
+ @interface RNArrangementPane : RCTViewComponentView
4
+ - (void)updateNativeFrame:(CGRect)frame;
5
+ @end
@@ -0,0 +1,47 @@
1
+ #import "RNArrangementPane.h"
2
+ #import "RNArrangementPaneShadowNode.h"
3
+ using namespace facebook::react;
4
+
5
+ @implementation RNArrangementPane {
6
+ ArrangementPaneShadowNode::ConcreteState::Shared _paneState;
7
+ CGRect _nativeFrame;
8
+ BOOL _hasNativeFrame;
9
+ }
10
+ + (ComponentDescriptorProvider)componentDescriptorProvider {
11
+ return concreteComponentDescriptorProvider<ArrangementPaneComponentDescriptor>();
12
+ }
13
+ + (BOOL)shouldBeRecycled { return NO; }
14
+ - (instancetype)initWithFrame:(CGRect)frame {
15
+ if (self = [super initWithFrame:frame]) {
16
+ _props = std::make_shared<const RNArrangementPaneProps>();
17
+ self.clipsToBounds = YES;
18
+ }
19
+ return self;
20
+ }
21
+ - (void)updateNativeFrame:(CGRect)frame {
22
+ _nativeFrame = frame;
23
+ _hasNativeFrame = YES;
24
+ self.frame = (CGRect){CGPointZero, frame.size};
25
+ [self synchronizeState];
26
+ }
27
+ - (void)synchronizeState {
28
+ if (!_paneState || !_hasNativeFrame) return;
29
+ const auto &old = _paneState->getData();
30
+ facebook::react::Size size{(Float)_nativeFrame.size.width, (Float)_nativeFrame.size.height};
31
+ facebook::react::Point origin{(Float)_nativeFrame.origin.x, (Float)_nativeFrame.origin.y};
32
+ if (old.measured && old.size == size && old.origin == origin) return;
33
+ _paneState->updateState(ArrangementPaneState{size, origin, true});
34
+ }
35
+ - (void)updateState:(State::Shared const &)state oldState:(State::Shared const &)oldState {
36
+ [super updateState:state oldState:oldState];
37
+ _paneState = std::static_pointer_cast<const ArrangementPaneShadowNode::ConcreteState>(state);
38
+ [self synchronizeState];
39
+ }
40
+ - (void)updateLayoutMetrics:(LayoutMetrics const &)metrics oldLayoutMetrics:(LayoutMetrics const &)oldMetrics {
41
+ // SwiftUI owns this view's frame. Fabric still receives metrics for descendants,
42
+ // onLayout and measurement; stale commits must not resize the native pane.
43
+ auto nativeMetrics = metrics;
44
+ if (_hasNativeFrame) nativeMetrics.frame = {{0, 0}, {(Float)_nativeFrame.size.width, (Float)_nativeFrame.size.height}};
45
+ [super updateLayoutMetrics:nativeMetrics oldLayoutMetrics:oldMetrics];
46
+ }
47
+ @end