craft-native 0.0.262 → 0.0.272
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/README.md +22 -0
- package/dist/android/templates/CraftNativeMutation.kt.template +2 -0
- package/dist/android/templates/MainActivityNative.kt.template +131 -10
- package/dist/browser/lifecycle.d.ts +7 -0
- package/dist/browser.js +126 -24
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/templates/CraftApp.swift +19 -3
- package/dist/ios/templates/CraftHybrid.swift +20 -0
- package/dist/ios/templates/CraftNativeActions.swift +108 -21
- package/dist/ios/templates/CraftNativeFlatList.swift +156 -16
- package/dist/ios/templates/CraftNativeMutation.swift +3 -1
- package/dist/ios/templates/CraftNativeScreen.swift +179 -15
- package/dist/macos/templates/CraftNativeMutation.swift +3 -1
- package/package.json +1 -1
|
@@ -2164,6 +2164,13 @@ extension UIView {
|
|
|
2164
2164
|
set { objc_setAssociatedObject(self, &craftNativeLayoutStyleKey, CraftNativeLayoutStyleBox(newValue), .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
|
|
2165
2165
|
}
|
|
2166
2166
|
|
|
2167
|
+
/// The margins this view's style asks for, for a container that places
|
|
2168
|
+
/// it without a flex parent (a FlatList cell).
|
|
2169
|
+
var craftLayoutMargins: UIEdgeInsets {
|
|
2170
|
+
let style = craftLayoutStyle
|
|
2171
|
+
return UIEdgeInsets(top: max(0, style.marginTop), left: max(0, style.marginLeft), bottom: max(0, style.marginBottom), right: max(0, style.marginRight))
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2167
2174
|
/// The corner radius the style asked for. The layer gets at most half the
|
|
2168
2175
|
/// shortest side: Core Animation draws nothing at all for a radius larger
|
|
2169
2176
|
/// than the view (`rounded-full` is 9999).
|
|
@@ -4328,9 +4335,13 @@ private final class CraftNativeScrollView: UIScrollView, UIGestureRecognizerDele
|
|
|
4328
4335
|
/// 3 quadratic, 4 close, in viewBox units - and scaled to the view. Paint is
|
|
4329
4336
|
/// inherited down the tree as SVG does; `dark:` classes arrive already
|
|
4330
4337
|
/// resolved for the appearance, so a redraw is all a theme change needs.
|
|
4331
|
-
|
|
4338
|
+
final class CraftNativeSvgView: UIView {
|
|
4332
4339
|
var drawing: [String: Any] = [:] {
|
|
4333
|
-
didSet {
|
|
4340
|
+
didSet {
|
|
4341
|
+
animates = Self.hasAnimation(drawing["children"] as? [Any] ?? [])
|
|
4342
|
+
setNeedsDisplay()
|
|
4343
|
+
updateFrames()
|
|
4344
|
+
}
|
|
4334
4345
|
}
|
|
4335
4346
|
|
|
4336
4347
|
override init(frame: CGRect) {
|
|
@@ -4338,10 +4349,123 @@ private final class CraftNativeSvgView: UIView {
|
|
|
4338
4349
|
isOpaque = false
|
|
4339
4350
|
backgroundColor = .clear
|
|
4340
4351
|
contentMode = .redraw
|
|
4352
|
+
NotificationCenter.default.addObserver(self, selector: #selector(reduceMotionChanged), name: UIAccessibility.reduceMotionStatusDidChangeNotification, object: nil)
|
|
4341
4353
|
}
|
|
4342
4354
|
|
|
4343
4355
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
4344
4356
|
|
|
4357
|
+
deinit { CraftNativeFrameClock.shared.cancel(self) }
|
|
4358
|
+
|
|
4359
|
+
// MARK: Animation
|
|
4360
|
+
|
|
4361
|
+
/// Whether a shape carries keyframes (SMIL `<animate attributeName="d">`,
|
|
4362
|
+
/// stx's `Animate`: an exercise's moving figure).
|
|
4363
|
+
private var animates = false
|
|
4364
|
+
/// When this drawing's repetition began: each one starts from its first
|
|
4365
|
+
/// pose when it comes on screen, as a page's SMIL starts when it loads.
|
|
4366
|
+
private var animationStart: CFTimeInterval?
|
|
4367
|
+
|
|
4368
|
+
/// Moving now: on display, and the person has not asked for less motion,
|
|
4369
|
+
/// when each shape keeps its own outline (the still pose).
|
|
4370
|
+
var isAnimating: Bool { animates && window != nil && !isHidden && !UIAccessibility.isReduceMotionEnabled }
|
|
4371
|
+
|
|
4372
|
+
override func didMoveToWindow() {
|
|
4373
|
+
super.didMoveToWindow()
|
|
4374
|
+
updateFrames()
|
|
4375
|
+
}
|
|
4376
|
+
|
|
4377
|
+
override var isHidden: Bool {
|
|
4378
|
+
didSet { if isHidden != oldValue { updateFrames() } }
|
|
4379
|
+
}
|
|
4380
|
+
|
|
4381
|
+
@objc private func reduceMotionChanged() {
|
|
4382
|
+
updateFrames()
|
|
4383
|
+
setNeedsDisplay()
|
|
4384
|
+
}
|
|
4385
|
+
|
|
4386
|
+
/// Frames from the screens' display link (CraftNativeFrameClock, up to
|
|
4387
|
+
/// 120Hz) while moving; none at all while still or out of sight.
|
|
4388
|
+
private func updateFrames() {
|
|
4389
|
+
if isAnimating {
|
|
4390
|
+
if animationStart == nil { animationStart = CACurrentMediaTime() }
|
|
4391
|
+
CraftNativeFrameClock.shared.request(self) { [weak self] in self?.nextFrame() }
|
|
4392
|
+
} else {
|
|
4393
|
+
animationStart = nil
|
|
4394
|
+
CraftNativeFrameClock.shared.cancel(self)
|
|
4395
|
+
}
|
|
4396
|
+
}
|
|
4397
|
+
|
|
4398
|
+
private func nextFrame() {
|
|
4399
|
+
guard isAnimating else { return updateFrames() }
|
|
4400
|
+
// Drawn only while some of it is on screen; a row scrolled away keeps
|
|
4401
|
+
// its time and is right when it comes back.
|
|
4402
|
+
if let window, window.bounds.intersects(convert(bounds, to: window)) { setNeedsDisplay() }
|
|
4403
|
+
CraftNativeFrameClock.shared.request(self) { [weak self] in self?.nextFrame() }
|
|
4404
|
+
}
|
|
4405
|
+
|
|
4406
|
+
private static func hasAnimation(_ children: [Any]) -> Bool {
|
|
4407
|
+
children.contains { child in
|
|
4408
|
+
guard let node = child as? [String: Any] else { return false }
|
|
4409
|
+
if node["type"] as? String == "Animate" { return (node["props"] as? [String: Any])?["frames"] != nil }
|
|
4410
|
+
return hasAnimation(node["children"] as? [Any] ?? [])
|
|
4411
|
+
}
|
|
4412
|
+
}
|
|
4413
|
+
|
|
4414
|
+
/// A shape's outline now: between the keyframes either side of `elapsed`
|
|
4415
|
+
/// seconds into its animation (SMIL's linear `calcMode`), or its own
|
|
4416
|
+
/// outline when it has none, the animation has ended, or motion is reduced.
|
|
4417
|
+
private func outline(of shape: [String: Any]) -> Any? {
|
|
4418
|
+
let geometry = (shape["props"] as? [String: Any])?["geometry"]
|
|
4419
|
+
guard isAnimating, let start = animationStart,
|
|
4420
|
+
let animate = (shape["children"] as? [Any])?.lazy.compactMap({ $0 as? [String: Any] }).first(where: { $0["type"] as? String == "Animate" }),
|
|
4421
|
+
let current = Self.keyframe(animate["props"] as? [String: Any] ?? [:], at: CACurrentMediaTime() - start) else { return geometry }
|
|
4422
|
+
return current
|
|
4423
|
+
}
|
|
4424
|
+
|
|
4425
|
+
/// The outline `elapsed` seconds into an `Animate`'s repetitions; nil
|
|
4426
|
+
/// when it does not animate path data or its repetitions are over.
|
|
4427
|
+
static func keyframe(_ props: [String: Any], at elapsed: CFTimeInterval) -> [Any]? {
|
|
4428
|
+
guard props["attributeName"] as? String == "d",
|
|
4429
|
+
let duration = number(props["duration"]), duration > 0, duration.isFinite,
|
|
4430
|
+
let frames = (props["frames"] as? [Any])?.compactMap({ ($0 as? [Any])?.compactMap(number) }),
|
|
4431
|
+
frames.count >= 2, elapsed >= 0 else { return nil }
|
|
4432
|
+
if let count = number(props["repeatCount"]), elapsed >= Double(count * duration) { return nil }
|
|
4433
|
+
let given = (props["frameTimes"] as? [Any])?.compactMap(number) ?? []
|
|
4434
|
+
let times = given.count == frames.count ? given : frames.indices.map { CGFloat($0) / CGFloat(frames.count - 1) }
|
|
4435
|
+
let phase = CGFloat(elapsed.truncatingRemainder(dividingBy: Double(duration))) / duration
|
|
4436
|
+
var index = 0
|
|
4437
|
+
while index < frames.count - 2, times[index + 1] <= phase { index += 1 }
|
|
4438
|
+
let from = frames[index]
|
|
4439
|
+
let to = frames[index + 1]
|
|
4440
|
+
let span = times[index + 1] - times[index]
|
|
4441
|
+
let share = span > 0 ? min(1, max(0, (phase - times[index]) / span)) : 0
|
|
4442
|
+
return interpolate(from, to, share).map { NSNumber(value: Double($0)) }
|
|
4443
|
+
}
|
|
4444
|
+
|
|
4445
|
+
/// Two outlines with the same commands, a share of the way from one to
|
|
4446
|
+
/// the other; the first as it is when their commands differ.
|
|
4447
|
+
static func interpolate(_ from: [CGFloat], _ to: [CGFloat], _ share: CGFloat) -> [CGFloat] {
|
|
4448
|
+
guard from.count == to.count, share > 0 else { return from }
|
|
4449
|
+
var out = from
|
|
4450
|
+
var i = 0
|
|
4451
|
+
while i < from.count {
|
|
4452
|
+
let command = Int(from[i])
|
|
4453
|
+
guard to[i] == from[i] else { return from }
|
|
4454
|
+
let count: Int
|
|
4455
|
+
switch command {
|
|
4456
|
+
case 0, 1: count = 2
|
|
4457
|
+
case 2: count = 6
|
|
4458
|
+
case 3: count = 4
|
|
4459
|
+
case 4: count = 0
|
|
4460
|
+
default: return from
|
|
4461
|
+
}
|
|
4462
|
+
guard i + count < from.count else { return from }
|
|
4463
|
+
if count > 0 { for k in 1...count { out[i + k] = from[i + k] + (to[i + k] - from[i + k]) * share } }
|
|
4464
|
+
i += count + 1
|
|
4465
|
+
}
|
|
4466
|
+
return out
|
|
4467
|
+
}
|
|
4468
|
+
|
|
4345
4469
|
var viewBox: CGRect? {
|
|
4346
4470
|
let props = drawing["props"] as? [String: Any] ?? [:]
|
|
4347
4471
|
guard let box = props["viewBox"] as? [Any], box.count == 4 else { return nil }
|
|
@@ -4461,8 +4585,10 @@ private final class CraftNativeSvgView: UIView {
|
|
|
4461
4585
|
draw(children: shape["children"] as? [Any] ?? [], paint: own, in: context)
|
|
4462
4586
|
case "SvgText":
|
|
4463
4587
|
drawText(shape, paint: own)
|
|
4588
|
+
case "Animate":
|
|
4589
|
+
continue
|
|
4464
4590
|
default:
|
|
4465
|
-
guard let path = CraftNativeSvgView.path((
|
|
4591
|
+
guard let path = CraftNativeSvgView.path(outline(of: shape)) else { continue }
|
|
4466
4592
|
context.saveGState()
|
|
4467
4593
|
path.usesEvenOddFillRule = own.fillRule == "evenodd"
|
|
4468
4594
|
if let fill = own.resolve(own.fill) {
|
|
@@ -4859,9 +4985,24 @@ enum CraftNativeScriptThread {
|
|
|
4859
4985
|
static let queue = DispatchQueue(label: "craft.native.script", qos: .userInteractive)
|
|
4860
4986
|
|
|
4861
4987
|
/// `work` where the screens' JavaScript runs: now on the main thread, or
|
|
4862
|
-
/// queued in order on the script thread.
|
|
4863
|
-
|
|
4864
|
-
|
|
4988
|
+
/// queued in order on the script thread. With frame pacing on
|
|
4989
|
+
/// (`CRAFT_FRAME_PACING=1`), work that waited or ran for 4ms or more is
|
|
4990
|
+
/// logged under `label`, so a slow screen can be told from a queued one.
|
|
4991
|
+
static func run(label: @autoclosure @escaping () -> String = "script", _ work: @escaping () -> Void) {
|
|
4992
|
+
let pacing = CraftFramePacing.shared
|
|
4993
|
+
guard pacing.enabled else {
|
|
4994
|
+
if isBackground { queue.async(execute: work) } else { work() }
|
|
4995
|
+
return
|
|
4996
|
+
}
|
|
4997
|
+
let queued = CACurrentMediaTime()
|
|
4998
|
+
let timed = {
|
|
4999
|
+
let started = CACurrentMediaTime()
|
|
5000
|
+
work()
|
|
5001
|
+
let ran = CACurrentMediaTime() - started
|
|
5002
|
+
let waited = started - queued
|
|
5003
|
+
if waited + ran >= 0.004 { pacing.scriptWork(label(), waited: waited, ran: ran) }
|
|
5004
|
+
}
|
|
5005
|
+
if isBackground { queue.async(execute: timed) } else { timed() }
|
|
4865
5006
|
}
|
|
4866
5007
|
|
|
4867
5008
|
/// From the script thread, `work` on the main thread, not waited for.
|
|
@@ -5241,7 +5382,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5241
5382
|
let pacing = CraftFramePacing.shared
|
|
5242
5383
|
let inputs = scriptInputs()
|
|
5243
5384
|
let name = routeName ?? "screen"
|
|
5244
|
-
CraftNativeScriptThread.run { [self] in
|
|
5385
|
+
CraftNativeScriptThread.run(label: "mount \(name)") { [self] in
|
|
5245
5386
|
pacing.measure("mount \(name) setup") { setupJavaScript(inputs) }
|
|
5246
5387
|
pacing.measure("mount \(name) bundle") { loadBundle() }
|
|
5247
5388
|
}
|
|
@@ -5302,8 +5443,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5302
5443
|
screenVisibilityChanged(false)
|
|
5303
5444
|
stopCelebrations(in: view)
|
|
5304
5445
|
// Release only this screen's accessory. A retained or prepared screen
|
|
5305
|
-
// can disappear after the destination has already published its own
|
|
5306
|
-
|
|
5446
|
+
// can disappear after the destination has already published its own;
|
|
5447
|
+
// released, it stays drawn until the destination says what it wants.
|
|
5448
|
+
CraftChrome.shared.releaseTabAccessory(ownedBy: self)
|
|
5307
5449
|
// Popped (or its stack dismissed) while the bar was away: it comes
|
|
5308
5450
|
// back for whatever shows next, a web page included.
|
|
5309
5451
|
if tabBarHidden, isMovingFromParent || isBeingDismissed || navigationController?.isBeingDismissed == true {
|
|
@@ -5348,7 +5490,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5348
5490
|
private func restoreShellChrome() {
|
|
5349
5491
|
guard !isAppScript else { return }
|
|
5350
5492
|
CraftChrome.shared.setHiddenByScreen(tabBarHidden)
|
|
5351
|
-
|
|
5493
|
+
// A screen not yet asked about the accessory (one shown for the first
|
|
5494
|
+
// time, before its script has run) leaves the bar as it is; one that
|
|
5495
|
+
// has drawn without asking for it takes off what nobody claims.
|
|
5496
|
+
if tabAccessoryDeclared { CraftChrome.shared.updateTabAccessory(tabAccessory, owner: self) }
|
|
5497
|
+
else if hybridRendered { CraftChrome.shared.clearUnclaimedTabAccessory() }
|
|
5352
5498
|
}
|
|
5353
5499
|
|
|
5354
5500
|
/// `requestAnimationFrame`: the next display frame, for a screen on display
|
|
@@ -5365,7 +5511,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5365
5511
|
guard frameWanted, !runtimeDisposed else { return }
|
|
5366
5512
|
frameWanted = false
|
|
5367
5513
|
let context = jsContext
|
|
5368
|
-
|
|
5514
|
+
let route = routeName ?? "screen"
|
|
5515
|
+
CraftNativeScriptThread.run(label: "frame \(route)") { context.objectForKeyedSubscript("__craftNativeFireFrame")?.call(withArguments: []) }
|
|
5369
5516
|
}
|
|
5370
5517
|
|
|
5371
5518
|
/// The screen came on screen or went off it: a screen pushed over it was
|
|
@@ -5400,7 +5547,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5400
5547
|
routeProps = props
|
|
5401
5548
|
guard isViewLoaded, !runtimeDisposed else { return }
|
|
5402
5549
|
let context = jsContext
|
|
5403
|
-
|
|
5550
|
+
let route = routeName ?? "screen"
|
|
5551
|
+
CraftNativeScriptThread.run(label: "params \(route)") {
|
|
5404
5552
|
context.setObject(params as NSDictionary, forKeyedSubscript: "__stxNativeParams" as NSString)
|
|
5405
5553
|
context.setObject(props as NSDictionary, forKeyedSubscript: "__stxNativeProps" as NSString)
|
|
5406
5554
|
}
|
|
@@ -5414,7 +5562,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5414
5562
|
CraftNativeFrameClock.shared.cancel(self)
|
|
5415
5563
|
disposeRenderedTree()
|
|
5416
5564
|
let context = jsContext
|
|
5417
|
-
|
|
5565
|
+
let route = routeName ?? "screen"
|
|
5566
|
+
CraftNativeScriptThread.run(label: "unmount \(route)") { context.objectForKeyedSubscript("__stxNativeUnmount")?.call(withArguments: []) }
|
|
5418
5567
|
audio.close()
|
|
5419
5568
|
sounds.close()
|
|
5420
5569
|
streams.close()
|
|
@@ -6215,6 +6364,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6215
6364
|
/// The app's tab bar away while this screen shows (`tabBarHidden`).
|
|
6216
6365
|
private var tabBarHidden = false
|
|
6217
6366
|
private var tabAccessory: [String: Any]?
|
|
6367
|
+
/// Whether the screen has said anything about the accessory yet
|
|
6368
|
+
/// (`tabAccessory`, null included): until it has, the bar keeps showing
|
|
6369
|
+
/// the one it had (CraftChrome.releaseTabAccessory).
|
|
6370
|
+
private var tabAccessoryDeclared = false
|
|
6218
6371
|
/// Whether a back-swipe may leave this screen at all, the edge's
|
|
6219
6372
|
/// included (`swipeBack: false`): a screen that steps through its own
|
|
6220
6373
|
/// pages with a Back of its own (a first-run tour's questions) would
|
|
@@ -6263,6 +6416,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6263
6416
|
guard !isAppScript else { return }
|
|
6264
6417
|
if options.keys.contains("tabAccessory") {
|
|
6265
6418
|
tabAccessory = options["tabAccessory"] as? [String: Any]
|
|
6419
|
+
tabAccessoryDeclared = true
|
|
6266
6420
|
if navigationController?.topViewController === self, view.window != nil {
|
|
6267
6421
|
CraftChrome.shared.updateTabAccessory(tabAccessory, owner: self)
|
|
6268
6422
|
}
|
|
@@ -6652,7 +6806,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6652
6806
|
let previous = info["previous"] as? String
|
|
6653
6807
|
self.send(type: "SESSION_CHANGED", payload: ["key": key, "token": token ?? NSNull(), "previous": previous ?? NSNull()])
|
|
6654
6808
|
let context = self.jsContext
|
|
6655
|
-
CraftNativeScriptThread.run { context.objectForKeyedSubscript("__craftSessionChanged")?.call(withArguments: [token ?? NSNull(), previous ?? NSNull()]) }
|
|
6809
|
+
CraftNativeScriptThread.run(label: "session \(self.routeName ?? "screen")") { context.objectForKeyedSubscript("__craftSessionChanged")?.call(withArguments: [token ?? NSNull(), previous ?? NSNull()]) }
|
|
6656
6810
|
})
|
|
6657
6811
|
}
|
|
6658
6812
|
if config.enablePushNotifications {
|
|
@@ -6937,8 +7091,15 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6937
7091
|
}
|
|
6938
7092
|
let module = payload["module"] as? String ?? ""
|
|
6939
7093
|
let method = payload["method"] as? String ?? ""
|
|
7094
|
+
// With frame pacing on: how long the capability took to answer,
|
|
7095
|
+
// which a screen's script waits on (a request, the keychain).
|
|
7096
|
+
let pacing = CraftFramePacing.shared
|
|
7097
|
+
let asked = pacing.enabled ? CACurrentMediaTime() : 0
|
|
7098
|
+
let route = routeName ?? "screen"
|
|
7099
|
+
let what = module == "Network" ? "\(module).\(method) \(((args.first as? [String: Any])?["url"] as? String).flatMap { URL(string: $0)?.path } ?? "")" : "\(module).\(method)"
|
|
6940
7100
|
let reply: (Result<Any, CraftNativeActionError>) -> Void = { [weak self] answer in
|
|
6941
7101
|
guard let self = self, self.pendingCapabilityRequests.remove(requestToken) != nil else { return }
|
|
7102
|
+
if pacing.enabled { pacing.capability("\(what) \(route)", took: CACurrentMediaTime() - asked) }
|
|
6942
7103
|
self.pendingCapabilityDeadlines.removeValue(forKey: requestToken)?.cancel()
|
|
6943
7104
|
self.screenRequestCancellers.removeValue(forKey: requestToken)
|
|
6944
7105
|
switch answer {
|
|
@@ -7167,6 +7328,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7167
7328
|
hybridRendered = true
|
|
7168
7329
|
// The app script's drawing is never on screen: not a first frame.
|
|
7169
7330
|
if !isAppScript { hybridEvents?.nativeScreenDidRender(self) }
|
|
7331
|
+
// Drawn without a word about the accessory: it wants none.
|
|
7332
|
+
if !isAppScript, !tabAccessoryDeclared, isShownOnScreen { CraftChrome.shared.clearUnclaimedTabAccessory() }
|
|
7170
7333
|
let waiting = renderWaiters
|
|
7171
7334
|
renderWaiters.removeAll()
|
|
7172
7335
|
waiting.forEach { $0() }
|
|
@@ -9832,7 +9995,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
9832
9995
|
guard let data = try? JSONSerialization.data(withJSONObject: message),
|
|
9833
9996
|
let json = String(data: data, encoding: .utf8) else { return }
|
|
9834
9997
|
let context = jsContext
|
|
9835
|
-
|
|
9998
|
+
let route = routeName ?? "screen"
|
|
9999
|
+
CraftNativeScriptThread.run(label: "\(type) \(route)") { context.objectForKeyedSubscript("__stxNativeCallback")?.call(withArguments: [json]) }
|
|
9836
10000
|
}
|
|
9837
10001
|
|
|
9838
10002
|
private func number(_ value: Any?) -> CGFloat? {
|
|
@@ -339,11 +339,13 @@ final class CraftNativeMutationDocument {
|
|
|
339
339
|
/// `Svg`). Only View, SafeAreaView and ScrollView used to be here, so a
|
|
340
340
|
/// drawing revealed after the first paint (an `:if` around an `<svg>`)
|
|
341
341
|
/// had its shapes refused as INVALID_PARENT, and the whole screen was
|
|
342
|
-
/// drawn again; Android's host already took the same list.
|
|
342
|
+
/// drawn again; Android's host already took the same list. A shape holds
|
|
343
|
+
/// its `Animate` (SMIL keyframes: HQ's moving exercise figures).
|
|
343
344
|
static let containerTypes: Set<String> = [
|
|
344
345
|
"View", "SafeAreaView", "ScrollView", "KeyboardAvoidingView", "Modal", "Pressable",
|
|
345
346
|
"TouchableOpacity", "TouchableHighlight", "ImageBackground", "FlatList", "SectionList",
|
|
346
347
|
"Fragment", "Svg", "G", "Map",
|
|
348
|
+
"Path", "Line", "Rect", "Circle", "Ellipse", "Polyline", "Polygon",
|
|
347
349
|
]
|
|
348
350
|
private static let containerTypeKeys = Set(containerTypes.map { $0.lowercased() })
|
|
349
351
|
|