@thelacanians/vue-native-cli 0.4.14 → 0.4.15
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.
- package/dist/cli.js +466 -191
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/NativeBridge.kt +38 -5
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VListFactory.kt +33 -13
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VScrollViewFactory.kt +27 -6
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VSliderFactory.kt +9 -2
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Helpers/EventThrottle.kt +57 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/EventThrottle.swift +79 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/NativeBridge.swift +106 -112
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VListFactory.swift +19 -2
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VScrollViewFactory.swift +9 -4
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VSliderFactory.swift +8 -3
- package/package.json +1 -1
package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/NativeBridge.kt
CHANGED
|
@@ -23,6 +23,20 @@ class NativeBridge(private val context: Context) {
|
|
|
23
23
|
"create", "createText", "appendChild", "insertBefore", "removeChild",
|
|
24
24
|
"setRootView", "setText", "setElementText"
|
|
25
25
|
)
|
|
26
|
+
|
|
27
|
+
/** Style properties that affect layout and require a layout pass when changed. */
|
|
28
|
+
private val layoutAffectingStyles = setOf(
|
|
29
|
+
"width", "height", "minWidth", "minHeight", "maxWidth", "maxHeight",
|
|
30
|
+
"flex", "flexGrow", "flexShrink", "flexBasis", "flexDirection",
|
|
31
|
+
"flexWrap", "alignItems", "alignSelf", "alignContent", "justifyContent",
|
|
32
|
+
"padding", "paddingTop", "paddingRight", "paddingBottom", "paddingLeft",
|
|
33
|
+
"paddingHorizontal", "paddingVertical", "paddingStart", "paddingEnd",
|
|
34
|
+
"margin", "marginTop", "marginRight", "marginBottom", "marginLeft",
|
|
35
|
+
"marginHorizontal", "marginVertical", "marginStart", "marginEnd",
|
|
36
|
+
"gap", "rowGap", "columnGap",
|
|
37
|
+
"position", "top", "right", "bottom", "left", "start", "end",
|
|
38
|
+
"aspectRatio", "display", "overflow", "direction",
|
|
39
|
+
)
|
|
26
40
|
}
|
|
27
41
|
|
|
28
42
|
private val mainHandler = Handler(Looper.getMainLooper())
|
|
@@ -81,20 +95,34 @@ class NativeBridge(private val context: Context) {
|
|
|
81
95
|
}
|
|
82
96
|
|
|
83
97
|
mainHandler.post {
|
|
84
|
-
var
|
|
98
|
+
var needsLayout = false
|
|
85
99
|
for (op in ops) {
|
|
86
100
|
try {
|
|
87
101
|
val opName = op.optString("op")
|
|
88
|
-
if (!
|
|
89
|
-
|
|
102
|
+
if (!needsLayout && opName in treeMutationOps) {
|
|
103
|
+
needsLayout = true
|
|
104
|
+
}
|
|
105
|
+
// Check if updateStyle changes any layout-affecting property
|
|
106
|
+
if (!needsLayout && opName == "updateStyle") {
|
|
107
|
+
val args = op.optJSONArray("args")
|
|
108
|
+
val styleObj = args?.optJSONObject(1)
|
|
109
|
+
if (styleObj != null) {
|
|
110
|
+
val keys = styleObj.keys()
|
|
111
|
+
while (keys.hasNext()) {
|
|
112
|
+
if (keys.next() in layoutAffectingStyles) {
|
|
113
|
+
needsLayout = true
|
|
114
|
+
break
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
90
118
|
}
|
|
91
119
|
handleOperation(op)
|
|
92
120
|
} catch (e: Exception) {
|
|
93
121
|
Log.e(TAG, "Error handling op '${op.optString("op")}': ${e.message}")
|
|
94
122
|
}
|
|
95
123
|
}
|
|
96
|
-
//
|
|
97
|
-
if (
|
|
124
|
+
// Trigger layout when tree was mutated or layout-affecting styles changed
|
|
125
|
+
if (needsLayout) {
|
|
98
126
|
triggerLayout()
|
|
99
127
|
}
|
|
100
128
|
}
|
|
@@ -380,6 +408,11 @@ class NativeBridge(private val context: Context) {
|
|
|
380
408
|
|
|
381
409
|
/** Clear all registries. Called during hot reload after JS teardown. Must run on main thread. */
|
|
382
410
|
fun clearAllRegistries() {
|
|
411
|
+
// Destroy all views via their factories before clearing maps
|
|
412
|
+
for ((_, view) in nodeViews) {
|
|
413
|
+
val factory = componentRegistry.factoryForView(view)
|
|
414
|
+
factory?.destroyView(view)
|
|
415
|
+
}
|
|
383
416
|
nodeViews.clear()
|
|
384
417
|
nodeTypes.clear()
|
|
385
418
|
eventHandlers.clear()
|
|
@@ -98,9 +98,16 @@ class VListFactory : NativeComponentFactory {
|
|
|
98
98
|
cumulativeX += dx
|
|
99
99
|
cumulativeY += dy
|
|
100
100
|
|
|
101
|
-
// Dispatch scroll event with cumulative position
|
|
101
|
+
// Dispatch scroll event with cumulative position and dimensions
|
|
102
102
|
scrollHandlers[recyclerView]?.invoke(
|
|
103
|
-
mapOf(
|
|
103
|
+
mapOf(
|
|
104
|
+
"x" to cumulativeX,
|
|
105
|
+
"y" to cumulativeY,
|
|
106
|
+
"contentWidth" to (recyclerView.computeHorizontalScrollRange()),
|
|
107
|
+
"contentHeight" to (recyclerView.computeVerticalScrollRange()),
|
|
108
|
+
"layoutWidth" to recyclerView.width,
|
|
109
|
+
"layoutHeight" to recyclerView.height,
|
|
110
|
+
)
|
|
104
111
|
)
|
|
105
112
|
|
|
106
113
|
// endReached detection (threshold = 20% from bottom)
|
|
@@ -167,20 +174,33 @@ class VListFactory : NativeComponentFactory {
|
|
|
167
174
|
}
|
|
168
175
|
|
|
169
176
|
class VListAdapter(private val items: List<View>) : RecyclerView.Adapter<VListAdapter.VH>() {
|
|
170
|
-
class VH(
|
|
177
|
+
class VH(itemView: View) : RecyclerView.ViewHolder(itemView)
|
|
171
178
|
|
|
172
179
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
|
|
173
|
-
val
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
)
|
|
180
|
-
|
|
180
|
+
val container = android.widget.FrameLayout(parent.context).apply {
|
|
181
|
+
layoutParams = RecyclerView.LayoutParams(
|
|
182
|
+
RecyclerView.LayoutParams.MATCH_PARENT,
|
|
183
|
+
RecyclerView.LayoutParams.WRAP_CONTENT
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
return VH(container)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
override fun onBindViewHolder(holder: VH, position: Int) {
|
|
190
|
+
val container = holder.itemView as android.widget.FrameLayout
|
|
191
|
+
// Remove the previous item view from this recycled container
|
|
192
|
+
container.removeAllViews()
|
|
193
|
+
val itemView = items.getOrNull(position) ?: return
|
|
194
|
+
// Remove from any existing parent (previous container or direct parent)
|
|
195
|
+
(itemView.parent as? ViewGroup)?.removeView(itemView)
|
|
196
|
+
container.addView(itemView, ViewGroup.LayoutParams(
|
|
197
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
198
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
199
|
+
))
|
|
181
200
|
}
|
|
182
201
|
|
|
183
|
-
override fun onBindViewHolder(holder: VH, position: Int) {}
|
|
184
202
|
override fun getItemCount(): Int = items.size
|
|
185
|
-
|
|
203
|
+
|
|
204
|
+
// Use a single view type so RecyclerView can reuse all ViewHolders
|
|
205
|
+
override fun getItemViewType(position: Int): Int = 0
|
|
186
206
|
}
|
|
@@ -64,6 +64,9 @@ class VScrollViewFactory : NativeComponentFactory {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
private val scrollThrottles = mutableMapOf<SwipeRefreshLayout, EventThrottle>()
|
|
68
|
+
private val scrollListeners = mutableMapOf<SwipeRefreshLayout, android.view.ViewTreeObserver.OnScrollChangedListener>()
|
|
69
|
+
|
|
67
70
|
override fun addEventListener(view: View, event: String, handler: (Any?) -> Unit) {
|
|
68
71
|
val srf = view as? SwipeRefreshLayout ?: return
|
|
69
72
|
val scroll = states[srf]?.scrollView
|
|
@@ -73,20 +76,38 @@ class VScrollViewFactory : NativeComponentFactory {
|
|
|
73
76
|
srf.setOnRefreshListener { handler(null) }
|
|
74
77
|
}
|
|
75
78
|
"scroll" -> {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
val throttle = EventThrottle(intervalMs = 16L, handler = handler)
|
|
80
|
+
scrollThrottles[srf] = throttle
|
|
81
|
+
val listener = android.view.ViewTreeObserver.OnScrollChangedListener {
|
|
82
|
+
val child = if (scroll != null && scroll.childCount > 0) scroll.getChildAt(0) else null
|
|
83
|
+
throttle.fire(mapOf(
|
|
84
|
+
"x" to (scroll?.scrollX ?: 0),
|
|
85
|
+
"y" to (scroll?.scrollY ?: 0),
|
|
86
|
+
"contentWidth" to (child?.width ?: scroll?.width ?: 0),
|
|
87
|
+
"contentHeight" to (child?.height ?: scroll?.height ?: 0),
|
|
88
|
+
"layoutWidth" to (scroll?.width ?: 0),
|
|
89
|
+
"layoutHeight" to (scroll?.height ?: 0),
|
|
79
90
|
))
|
|
80
91
|
}
|
|
92
|
+
scrollListeners[srf] = listener
|
|
93
|
+
scroll?.viewTreeObserver?.addOnScrollChangedListener(listener)
|
|
81
94
|
}
|
|
82
95
|
}
|
|
83
96
|
}
|
|
84
97
|
|
|
85
98
|
override fun removeEventListener(view: View, event: String) {
|
|
86
99
|
val srf = view as? SwipeRefreshLayout ?: return
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
100
|
+
when (event) {
|
|
101
|
+
"refresh" -> {
|
|
102
|
+
srf.setOnRefreshListener(null)
|
|
103
|
+
srf.isEnabled = false
|
|
104
|
+
}
|
|
105
|
+
"scroll" -> {
|
|
106
|
+
scrollListeners.remove(srf)?.let { listener ->
|
|
107
|
+
states[srf]?.scrollView?.viewTreeObserver?.removeOnScrollChangedListener(listener)
|
|
108
|
+
}
|
|
109
|
+
scrollThrottles.remove(srf)?.cancel()
|
|
110
|
+
}
|
|
90
111
|
}
|
|
91
112
|
}
|
|
92
113
|
|
|
@@ -48,18 +48,24 @@ class VSliderFactory : NativeComponentFactory {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
private val throttles = mutableMapOf<SeekBar, EventThrottle>()
|
|
52
|
+
|
|
51
53
|
override fun addEventListener(view: View, event: String, handler: (Any?) -> Unit) {
|
|
52
54
|
val sb = view as? SeekBar ?: return
|
|
53
55
|
when (event) {
|
|
54
56
|
"change", "valueChange" -> {
|
|
55
57
|
changeHandlers[sb] = handler
|
|
58
|
+
val throttle = EventThrottle(intervalMs = 16L, handler = handler)
|
|
59
|
+
throttles[sb] = throttle
|
|
56
60
|
sb.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
|
57
61
|
override fun onProgressChanged(s: SeekBar, progress: Int, fromUser: Boolean) {
|
|
58
|
-
if (fromUser)
|
|
62
|
+
if (fromUser) throttle.fire(mapOf("value" to progress.toFloat() / s.max.toFloat()))
|
|
59
63
|
}
|
|
60
64
|
override fun onStartTrackingTouch(s: SeekBar) {}
|
|
61
65
|
override fun onStopTrackingTouch(s: SeekBar) {
|
|
62
|
-
|
|
66
|
+
// Always deliver the final value immediately on release
|
|
67
|
+
throttle.cancel()
|
|
68
|
+
changeHandlers[s]?.invoke(mapOf("value" to s.progress.toFloat() / s.max.toFloat()))
|
|
63
69
|
}
|
|
64
70
|
})
|
|
65
71
|
}
|
|
@@ -69,6 +75,7 @@ class VSliderFactory : NativeComponentFactory {
|
|
|
69
75
|
override fun removeEventListener(view: View, event: String) {
|
|
70
76
|
val sb = view as? SeekBar ?: return
|
|
71
77
|
changeHandlers.remove(sb)
|
|
78
|
+
throttles.remove(sb)?.cancel()
|
|
72
79
|
sb.setOnSeekBarChangeListener(null)
|
|
73
80
|
}
|
|
74
81
|
}
|
package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Helpers/EventThrottle.kt
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
package com.vuenative.core
|
|
2
|
+
|
|
3
|
+
import android.os.Handler
|
|
4
|
+
import android.os.Looper
|
|
5
|
+
import android.os.SystemClock
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Throttles high-frequency event handlers to avoid flooding the JS bridge.
|
|
9
|
+
*
|
|
10
|
+
* When a high-frequency event (scroll, slider drag) fires many times per frame,
|
|
11
|
+
* each invocation becomes a bridge round-trip. This utility ensures at most one
|
|
12
|
+
* call per [intervalMs] milliseconds, with a trailing call to deliver the latest value.
|
|
13
|
+
*
|
|
14
|
+
* Default interval: 16ms (~60 FPS).
|
|
15
|
+
*/
|
|
16
|
+
class EventThrottle(
|
|
17
|
+
private val intervalMs: Long = 16L,
|
|
18
|
+
private val handler: (Any?) -> Unit
|
|
19
|
+
) {
|
|
20
|
+
private var lastFireTime: Long = 0
|
|
21
|
+
private var pendingTrailing = false
|
|
22
|
+
private var latestPayload: Any? = null
|
|
23
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
24
|
+
|
|
25
|
+
private val trailingRunnable = Runnable {
|
|
26
|
+
lastFireTime = SystemClock.uptimeMillis()
|
|
27
|
+
pendingTrailing = false
|
|
28
|
+
handler(latestPayload)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Call this from the native event callback instead of the original handler.
|
|
33
|
+
* Fires immediately if enough time has elapsed, otherwise schedules a trailing call.
|
|
34
|
+
*/
|
|
35
|
+
fun fire(payload: Any?) {
|
|
36
|
+
val now = SystemClock.uptimeMillis()
|
|
37
|
+
val elapsed = now - lastFireTime
|
|
38
|
+
latestPayload = payload
|
|
39
|
+
|
|
40
|
+
if (elapsed >= intervalMs) {
|
|
41
|
+
lastFireTime = now
|
|
42
|
+
pendingTrailing = false
|
|
43
|
+
mainHandler.removeCallbacks(trailingRunnable)
|
|
44
|
+
handler(payload)
|
|
45
|
+
} else if (!pendingTrailing) {
|
|
46
|
+
pendingTrailing = true
|
|
47
|
+
mainHandler.postDelayed(trailingRunnable, intervalMs - elapsed)
|
|
48
|
+
}
|
|
49
|
+
// If trailing is already pending, latestPayload is updated above
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Cancel any pending trailing call. */
|
|
53
|
+
fun cancel() {
|
|
54
|
+
mainHandler.removeCallbacks(trailingRunnable)
|
|
55
|
+
pendingTrailing = false
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#if canImport(UIKit)
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
/// Throttles high-frequency event handlers to avoid flooding the JS bridge.
|
|
5
|
+
///
|
|
6
|
+
/// When a high-frequency event (scroll, slider drag, text input) fires many
|
|
7
|
+
/// times per frame, each invocation becomes a bridge round-trip. This utility
|
|
8
|
+
/// ensures at most one call per `interval` seconds, with a trailing call
|
|
9
|
+
/// to deliver the latest value.
|
|
10
|
+
///
|
|
11
|
+
/// Default interval: 16ms (~60 FPS). Callers can customize via `interval`.
|
|
12
|
+
final class EventThrottle {
|
|
13
|
+
|
|
14
|
+
/// Minimum time between handler invocations (seconds).
|
|
15
|
+
let interval: TimeInterval
|
|
16
|
+
|
|
17
|
+
/// The throttled handler to call.
|
|
18
|
+
private let handler: (Any?) -> Void
|
|
19
|
+
|
|
20
|
+
/// Timestamp of the last invocation.
|
|
21
|
+
private var lastFireTime: CFTimeInterval = 0
|
|
22
|
+
|
|
23
|
+
/// Whether a trailing call is pending.
|
|
24
|
+
private var pendingTrailing = false
|
|
25
|
+
|
|
26
|
+
/// The most recent payload, used for the trailing call.
|
|
27
|
+
private var latestPayload: Any?
|
|
28
|
+
|
|
29
|
+
/// Timer for delivering the trailing call.
|
|
30
|
+
private var trailingTimer: DispatchSourceTimer?
|
|
31
|
+
|
|
32
|
+
/// Create a throttled event handler.
|
|
33
|
+
/// - Parameters:
|
|
34
|
+
/// - interval: Minimum seconds between invocations. Default 0.016 (~60fps).
|
|
35
|
+
/// - handler: The closure to invoke with the event payload.
|
|
36
|
+
init(interval: TimeInterval = 0.016, handler: @escaping (Any?) -> Void) {
|
|
37
|
+
self.interval = interval
|
|
38
|
+
self.handler = handler
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
deinit {
|
|
42
|
+
trailingTimer?.cancel()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Call this from the native event callback instead of the original handler.
|
|
46
|
+
/// Fires immediately if enough time has elapsed, otherwise schedules a trailing call.
|
|
47
|
+
func fire(_ payload: Any?) {
|
|
48
|
+
let now = CACurrentMediaTime()
|
|
49
|
+
let elapsed = now - lastFireTime
|
|
50
|
+
|
|
51
|
+
latestPayload = payload
|
|
52
|
+
|
|
53
|
+
if elapsed >= interval {
|
|
54
|
+
// Enough time has passed — fire immediately
|
|
55
|
+
lastFireTime = now
|
|
56
|
+
pendingTrailing = false
|
|
57
|
+
trailingTimer?.cancel()
|
|
58
|
+
trailingTimer = nil
|
|
59
|
+
handler(payload)
|
|
60
|
+
} else if !pendingTrailing {
|
|
61
|
+
// Schedule a trailing call for the remaining time
|
|
62
|
+
pendingTrailing = true
|
|
63
|
+
let remaining = interval - elapsed
|
|
64
|
+
let timer = DispatchSource.makeTimerSource(queue: .main)
|
|
65
|
+
timer.schedule(deadline: .now() + remaining)
|
|
66
|
+
timer.setEventHandler { [weak self] in
|
|
67
|
+
guard let self = self else { return }
|
|
68
|
+
self.lastFireTime = CACurrentMediaTime()
|
|
69
|
+
self.pendingTrailing = false
|
|
70
|
+
self.trailingTimer = nil
|
|
71
|
+
self.handler(self.latestPayload)
|
|
72
|
+
}
|
|
73
|
+
trailingTimer = timer
|
|
74
|
+
timer.resume()
|
|
75
|
+
}
|
|
76
|
+
// If a trailing call is already pending, just update latestPayload (done above)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
#endif
|
|
@@ -69,6 +69,59 @@ public final class NativeBridge {
|
|
|
69
69
|
|
|
70
70
|
private static var traitObserverKey: UInt8 = 99
|
|
71
71
|
|
|
72
|
+
// MARK: - Bridge Function Registration
|
|
73
|
+
|
|
74
|
+
/// Register `__VN_flushOperations`, `__VN_teardown`, `__VN_log`, and `__VN_handleError`
|
|
75
|
+
/// on the given JSContext. Called from both `initialize()` and `reloadWithBundle()`.
|
|
76
|
+
private func registerBridgeFunctions(on context: JSContext) {
|
|
77
|
+
let flushOps: @convention(block) (JSValue) -> Void = { [weak self] opsValue in
|
|
78
|
+
guard let self = self else { return }
|
|
79
|
+
guard let jsonString = opsValue.toString(), !jsonString.isEmpty else {
|
|
80
|
+
NSLog("[VueNative Bridge] Warning: Empty operations batch")
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
guard let data = jsonString.data(using: .utf8),
|
|
85
|
+
let operations = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else {
|
|
86
|
+
NSLog("[VueNative Bridge] Error: Failed to parse operations JSON")
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
DispatchQueue.main.async { [weak self] in
|
|
91
|
+
self?.processOperations(operations)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
context.setObject(flushOps, forKeyedSubscript: "__VN_flushOperations" as NSString)
|
|
95
|
+
|
|
96
|
+
let teardown: @convention(block) () -> Void = { [weak self] in
|
|
97
|
+
NSLog("[VueNative] Teardown called from JS")
|
|
98
|
+
_ = self
|
|
99
|
+
}
|
|
100
|
+
context.setObject(teardown, forKeyedSubscript: "__VN_teardown" as NSString)
|
|
101
|
+
|
|
102
|
+
let log: @convention(block) (JSValue) -> Void = { message in
|
|
103
|
+
NSLog("[VueNative JS] \(message.toString() ?? "")")
|
|
104
|
+
}
|
|
105
|
+
context.setObject(log, forKeyedSubscript: "__VN_log" as NSString)
|
|
106
|
+
|
|
107
|
+
let handleError: @convention(block) (JSValue) -> Void = { errorInfoValue in
|
|
108
|
+
let jsonString = errorInfoValue.toString() ?? "{}"
|
|
109
|
+
NSLog("[VueNative Error] %@", jsonString)
|
|
110
|
+
|
|
111
|
+
if let data = jsonString.data(using: .utf8),
|
|
112
|
+
let info = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
|
113
|
+
let message = info["message"] as? String ?? "Unknown error"
|
|
114
|
+
let stack = info["stack"] as? String ?? ""
|
|
115
|
+
let componentName = info["componentName"] as? String ?? "unknown"
|
|
116
|
+
NSLog("[VueNative Error] Component: %@, Message: %@", componentName, message)
|
|
117
|
+
if !stack.isEmpty {
|
|
118
|
+
NSLog("[VueNative Error] Stack: %@", stack)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
context.setObject(handleError, forKeyedSubscript: "__VN_handleError" as NSString)
|
|
123
|
+
}
|
|
124
|
+
|
|
72
125
|
// MARK: - Setup
|
|
73
126
|
|
|
74
127
|
/// Initialize the bridge. Must be called after JSRuntime.initialize().
|
|
@@ -83,61 +136,7 @@ public final class NativeBridge {
|
|
|
83
136
|
runtime.jsQueue.async { [weak self] in
|
|
84
137
|
guard let self = self, let context = runtime.context else { return }
|
|
85
138
|
|
|
86
|
-
|
|
87
|
-
guard let self = self else { return }
|
|
88
|
-
guard let jsonString = opsValue.toString(), !jsonString.isEmpty else {
|
|
89
|
-
NSLog("[VueNative Bridge] Warning: Empty operations batch")
|
|
90
|
-
return
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Parse JSON on the JS queue (avoid main thread work)
|
|
94
|
-
guard let data = jsonString.data(using: .utf8),
|
|
95
|
-
let operations = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else {
|
|
96
|
-
NSLog("[VueNative Bridge] Error: Failed to parse operations JSON")
|
|
97
|
-
return
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Process operations on the main thread
|
|
101
|
-
DispatchQueue.main.async { [weak self] in
|
|
102
|
-
self?.processOperations(operations)
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
context.setObject(flushOps, forKeyedSubscript: "__VN_flushOperations" as NSString)
|
|
106
|
-
|
|
107
|
-
// Register __VN_teardown for graceful JS-side shutdown on hot reload
|
|
108
|
-
let teardown: @convention(block) () -> Void = { [weak self] in
|
|
109
|
-
// Graceful shutdown: bridge will be re-initialized on reload
|
|
110
|
-
NSLog("[VueNative] Teardown called from JS")
|
|
111
|
-
_ = self
|
|
112
|
-
}
|
|
113
|
-
context.setObject(teardown, forKeyedSubscript: "__VN_teardown" as NSString)
|
|
114
|
-
|
|
115
|
-
// Register __VN_log for debug logging from JS
|
|
116
|
-
let log: @convention(block) (JSValue) -> Void = { message in
|
|
117
|
-
NSLog("[VueNative JS] \(message.toString() ?? "")")
|
|
118
|
-
}
|
|
119
|
-
context.setObject(log, forKeyedSubscript: "__VN_log" as NSString)
|
|
120
|
-
|
|
121
|
-
// Register __VN_handleError for JS error reporting
|
|
122
|
-
// Called from JS with a JSON-encoded string containing error info
|
|
123
|
-
// (message, stack, componentName).
|
|
124
|
-
let handleError: @convention(block) (JSValue) -> Void = { errorInfoValue in
|
|
125
|
-
let jsonString = errorInfoValue.toString() ?? "{}"
|
|
126
|
-
NSLog("[VueNative Error] %@", jsonString)
|
|
127
|
-
|
|
128
|
-
// Try to extract structured fields for clearer logging
|
|
129
|
-
if let data = jsonString.data(using: .utf8),
|
|
130
|
-
let info = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
|
131
|
-
let message = info["message"] as? String ?? "Unknown error"
|
|
132
|
-
let stack = info["stack"] as? String ?? ""
|
|
133
|
-
let componentName = info["componentName"] as? String ?? "unknown"
|
|
134
|
-
NSLog("[VueNative Error] Component: %@, Message: %@", componentName, message)
|
|
135
|
-
if !stack.isEmpty {
|
|
136
|
-
NSLog("[VueNative Error] Stack: %@", stack)
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
context.setObject(handleError, forKeyedSubscript: "__VN_handleError" as NSString)
|
|
139
|
+
self.registerBridgeFunctions(on: context)
|
|
141
140
|
}
|
|
142
141
|
|
|
143
142
|
// Register all native modules synchronously so they are available
|
|
@@ -153,19 +152,44 @@ public final class NativeBridge {
|
|
|
153
152
|
"setRootView", "setText", "setElementText"
|
|
154
153
|
]
|
|
155
154
|
|
|
155
|
+
/// Style properties that affect Yoga layout and require a layout pass when changed.
|
|
156
|
+
/// When a batch contains only updateStyle ops for non-layout properties (e.g.
|
|
157
|
+
/// backgroundColor, opacity), we skip the expensive layout recalculation.
|
|
158
|
+
private static let layoutAffectingStyles: Set<String> = [
|
|
159
|
+
// Dimensions
|
|
160
|
+
"width", "height", "minWidth", "minHeight", "maxWidth", "maxHeight",
|
|
161
|
+
// Flex
|
|
162
|
+
"flex", "flexGrow", "flexShrink", "flexBasis", "flexDirection",
|
|
163
|
+
"flexWrap", "alignItems", "alignSelf", "alignContent", "justifyContent",
|
|
164
|
+
// Spacing
|
|
165
|
+
"padding", "paddingTop", "paddingRight", "paddingBottom", "paddingLeft",
|
|
166
|
+
"paddingHorizontal", "paddingVertical", "paddingStart", "paddingEnd",
|
|
167
|
+
"margin", "marginTop", "marginRight", "marginBottom", "marginLeft",
|
|
168
|
+
"marginHorizontal", "marginVertical", "marginStart", "marginEnd",
|
|
169
|
+
// Gap
|
|
170
|
+
"gap", "rowGap", "columnGap",
|
|
171
|
+
// Position
|
|
172
|
+
"position", "top", "right", "bottom", "left", "start", "end",
|
|
173
|
+
// Other layout
|
|
174
|
+
"aspectRatio", "display", "overflow", "direction",
|
|
175
|
+
]
|
|
176
|
+
|
|
156
177
|
/// Process a batch of operations on the main thread.
|
|
157
178
|
/// Each operation has an "op" key and "args" array.
|
|
158
|
-
///
|
|
159
|
-
/// (create, appendChild, insertBefore, removeChild, etc.)
|
|
160
|
-
///
|
|
179
|
+
/// Triggers a Yoga layout pass when the batch contains tree mutations
|
|
180
|
+
/// (create, appendChild, insertBefore, removeChild, etc.) OR style changes
|
|
181
|
+
/// that affect layout (width, height, padding, margin, flex, etc.).
|
|
182
|
+
/// Batches that only update visual styles/events skip the expensive layout.
|
|
161
183
|
///
|
|
162
184
|
/// Access: internal (not private) so that `@testable import` can exercise
|
|
163
185
|
/// operation handling without going through JSContext.
|
|
164
186
|
func processOperations(_ operations: [[String: Any]]) {
|
|
165
187
|
dispatchPrecondition(condition: .onQueue(.main))
|
|
188
|
+
#if DEBUG
|
|
166
189
|
NSLog("[VueNative Bridge] Processing %d operations", operations.count)
|
|
190
|
+
#endif
|
|
167
191
|
|
|
168
|
-
var
|
|
192
|
+
var needsLayout = false
|
|
169
193
|
|
|
170
194
|
for operation in operations {
|
|
171
195
|
guard let op = operation["op"] as? String,
|
|
@@ -174,8 +198,20 @@ public final class NativeBridge {
|
|
|
174
198
|
continue
|
|
175
199
|
}
|
|
176
200
|
|
|
177
|
-
if !
|
|
178
|
-
|
|
201
|
+
if !needsLayout && NativeBridge.treeMutationOps.contains(op) {
|
|
202
|
+
needsLayout = true
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Check if updateStyle changes any layout-affecting property
|
|
206
|
+
if !needsLayout && op == "updateStyle",
|
|
207
|
+
args.count >= 2,
|
|
208
|
+
let styles = args[1] as? [String: Any] {
|
|
209
|
+
for key in styles.keys {
|
|
210
|
+
if NativeBridge.layoutAffectingStyles.contains(key) {
|
|
211
|
+
needsLayout = true
|
|
212
|
+
break
|
|
213
|
+
}
|
|
214
|
+
}
|
|
179
215
|
}
|
|
180
216
|
|
|
181
217
|
switch op {
|
|
@@ -212,8 +248,8 @@ public final class NativeBridge {
|
|
|
212
248
|
}
|
|
213
249
|
}
|
|
214
250
|
|
|
215
|
-
//
|
|
216
|
-
if
|
|
251
|
+
// Trigger layout when tree was mutated or layout-affecting styles changed
|
|
252
|
+
if needsLayout {
|
|
217
253
|
triggerLayout()
|
|
218
254
|
}
|
|
219
255
|
}
|
|
@@ -388,8 +424,12 @@ public final class NativeBridge {
|
|
|
388
424
|
if let factory = ComponentRegistry.factory(for: parentView) {
|
|
389
425
|
factory.insertChild(childView, into: container, before: beforeView)
|
|
390
426
|
} else if let index = container.subviews.firstIndex(of: beforeView) {
|
|
391
|
-
container.flex.addItem(childView)
|
|
392
427
|
container.insertSubview(childView, at: index)
|
|
428
|
+
// Rebuild Yoga children to match UIView subview order
|
|
429
|
+
container.flex.removeAllChildren()
|
|
430
|
+
for subview in container.subviews {
|
|
431
|
+
container.flex.addItem(subview)
|
|
432
|
+
}
|
|
393
433
|
} else {
|
|
394
434
|
container.flex.addItem(childView)
|
|
395
435
|
}
|
|
@@ -781,55 +821,9 @@ public final class NativeBridge {
|
|
|
781
821
|
return
|
|
782
822
|
}
|
|
783
823
|
|
|
784
|
-
// Re-register
|
|
824
|
+
// Re-register bridge functions on new context
|
|
785
825
|
guard let context = self.runtime.context else { return }
|
|
786
|
-
|
|
787
|
-
let flushOps: @convention(block) (JSValue) -> Void = { [weak self] opsValue in
|
|
788
|
-
guard let self = self else { return }
|
|
789
|
-
guard let jsonString = opsValue.toString(), !jsonString.isEmpty else {
|
|
790
|
-
NSLog("[VueNative Bridge] Warning: Empty operations batch")
|
|
791
|
-
return
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
guard let data = jsonString.data(using: .utf8),
|
|
795
|
-
let operations = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else {
|
|
796
|
-
NSLog("[VueNative Bridge] Error: Failed to parse operations JSON")
|
|
797
|
-
return
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
DispatchQueue.main.async { [weak self] in
|
|
801
|
-
self?.processOperations(operations)
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
context.setObject(flushOps, forKeyedSubscript: "__VN_flushOperations" as NSString)
|
|
805
|
-
|
|
806
|
-
let teardown: @convention(block) () -> Void = { [weak self] in
|
|
807
|
-
NSLog("[VueNative] Teardown called from JS")
|
|
808
|
-
_ = self
|
|
809
|
-
}
|
|
810
|
-
context.setObject(teardown, forKeyedSubscript: "__VN_teardown" as NSString)
|
|
811
|
-
|
|
812
|
-
let log: @convention(block) (JSValue) -> Void = { message in
|
|
813
|
-
NSLog("[VueNative JS] \(message.toString() ?? "")")
|
|
814
|
-
}
|
|
815
|
-
context.setObject(log, forKeyedSubscript: "__VN_log" as NSString)
|
|
816
|
-
|
|
817
|
-
let handleError: @convention(block) (JSValue) -> Void = { errorInfoValue in
|
|
818
|
-
let jsonString = errorInfoValue.toString() ?? "{}"
|
|
819
|
-
NSLog("[VueNative Error] %@", jsonString)
|
|
820
|
-
|
|
821
|
-
if let data = jsonString.data(using: .utf8),
|
|
822
|
-
let info = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
|
823
|
-
let message = info["message"] as? String ?? "Unknown error"
|
|
824
|
-
let stack = info["stack"] as? String ?? ""
|
|
825
|
-
let componentName = info["componentName"] as? String ?? "unknown"
|
|
826
|
-
NSLog("[VueNative Error] Component: %@, Message: %@", componentName, message)
|
|
827
|
-
if !stack.isEmpty {
|
|
828
|
-
NSLog("[VueNative Error] Stack: %@", stack)
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
context.setObject(handleError, forKeyedSubscript: "__VN_handleError" as NSString)
|
|
826
|
+
self.registerBridgeFunctions(on: context)
|
|
833
827
|
|
|
834
828
|
NSLog("[VueNative Bridge] reloadWithBundle: bridge re-registered on new context")
|
|
835
829
|
}
|