@thelacanians/vue-native-cli 0.4.13 → 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.
@@ -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
- let flushOps: @convention(block) (JSValue) -> Void = { [weak self] opsValue in
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
- /// Only triggers a Yoga layout pass when the batch contains tree mutations
159
- /// (create, appendChild, insertBefore, removeChild, etc.). Batches that
160
- /// only update props/styles/events skip the expensive layout recalculation.
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 hasMutations = false
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 !hasMutations && NativeBridge.treeMutationOps.contains(op) {
178
- hasMutations = true
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
- // Only trigger layout recalculation when the batch mutated the view tree
216
- if hasMutations {
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 __VN_flushOperations, __VN_teardown, __VN_log on new context
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
  }
@@ -37,6 +37,7 @@ final class VListFactory: NativeComponentFactory {
37
37
  switch event {
38
38
  case "scroll":
39
39
  container.onScroll = handler
40
+ container.scrollThrottle = EventThrottle(interval: 0.016, handler: handler)
40
41
  case "endReached":
41
42
  container.onEndReached = handler
42
43
  default:
@@ -47,7 +48,9 @@ final class VListFactory: NativeComponentFactory {
47
48
  func removeEventListener(view: UIView, event: String) {
48
49
  guard let container = view as? VListContainerView else { return }
49
50
  switch event {
50
- case "scroll": container.onScroll = nil
51
+ case "scroll":
52
+ container.onScroll = nil
53
+ container.scrollThrottle = nil
51
54
  case "endReached": container.onEndReached = nil
52
55
  default: break
53
56
  }
@@ -106,6 +109,7 @@ final class VListContainerView: UIView {
106
109
  var itemViews: [UIView] = []
107
110
  var estimatedItemHeight: CGFloat = 44
108
111
  var onScroll: ((Any?) -> Void)?
112
+ var scrollThrottle: EventThrottle?
109
113
  var onEndReached: ((Any?) -> Void)?
110
114
  fileprivate var firedEndReached = false
111
115
  private lazy var internalDelegate = VListInternalDelegate(container: self)
@@ -195,7 +199,20 @@ private final class VListInternalDelegate: NSObject,
195
199
  func scrollViewDidScroll(_ scrollView: UIScrollView) {
196
200
  guard let container = container else { return }
197
201
  let offset = scrollView.contentOffset
198
- container.onScroll?(["x": Double(offset.x), "y": Double(offset.y)])
202
+ let payload: [String: Any] = [
203
+ "x": Double(offset.x),
204
+ "y": Double(offset.y),
205
+ "contentWidth": Double(scrollView.contentSize.width),
206
+ "contentHeight": Double(scrollView.contentSize.height),
207
+ "layoutWidth": Double(scrollView.frame.width),
208
+ "layoutHeight": Double(scrollView.frame.height),
209
+ ]
210
+ // Use throttle if available, otherwise fire directly
211
+ if let throttle = container.scrollThrottle {
212
+ throttle.fire(payload)
213
+ } else {
214
+ container.onScroll?(payload)
215
+ }
199
216
 
200
217
  // endReached detection (threshold = 20% from bottom)
201
218
  let contentH = scrollView.contentSize.height
@@ -221,20 +221,25 @@ private final class RefreshTarget: NSObject {
221
221
  // MARK: - ScrollDelegateProxy
222
222
 
223
223
  /// UIScrollViewDelegate proxy that forwards scroll events to a JS handler.
224
+ /// Uses EventThrottle to limit bridge round-trips to ~60/s during fast scrolling.
224
225
  private final class ScrollDelegateProxy: NSObject, UIScrollViewDelegate {
225
- private let handler: (Any?) -> Void
226
+ private let throttle: EventThrottle
226
227
 
227
228
  init(handler: @escaping (Any?) -> Void) {
228
- self.handler = handler
229
+ self.throttle = EventThrottle(interval: 0.016, handler: handler)
229
230
  super.init()
230
231
  }
231
232
 
232
233
  func scrollViewDidScroll(_ scrollView: UIScrollView) {
233
234
  let payload: [String: Any] = [
234
235
  "x": scrollView.contentOffset.x,
235
- "y": scrollView.contentOffset.y
236
+ "y": scrollView.contentOffset.y,
237
+ "contentWidth": scrollView.contentSize.width,
238
+ "contentHeight": scrollView.contentSize.height,
239
+ "layoutWidth": scrollView.frame.width,
240
+ "layoutHeight": scrollView.frame.height,
236
241
  ]
237
- handler(payload)
242
+ throttle.fire(payload)
238
243
  }
239
244
  }
240
245
  #endif
@@ -53,11 +53,16 @@ final class VSliderFactory: NativeComponentFactory {
53
53
  }
54
54
  }
55
55
 
56
+ /// Throttled target for UISlider .valueChanged events.
57
+ /// Limits bridge round-trips to ~60/s during continuous slider dragging.
56
58
  private final class SliderTarget: NSObject {
57
- let handler: (Any?) -> Void
58
- init(handler: @escaping (Any?) -> Void) { self.handler = handler }
59
+ private let throttle: EventThrottle
60
+ init(handler: @escaping (Any?) -> Void) {
61
+ self.throttle = EventThrottle(interval: 0.016, handler: handler)
62
+ super.init()
63
+ }
59
64
  @objc func handleValueChanged(_ slider: UISlider) {
60
- handler(Double(slider.value))
65
+ throttle.fire(["value": Double(slider.value)])
61
66
  }
62
67
  }
63
68
  #endif
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thelacanians/vue-native-cli",
3
- "version": "0.4.13",
3
+ "version": "0.4.15",
4
4
  "description": "CLI for creating and running Vue Native apps",
5
5
  "license": "MIT",
6
6
  "author": "Vue Native Contributors",