@craft-native/ios 0.0.484 → 0.0.486
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/package.json
CHANGED
|
@@ -5192,7 +5192,164 @@ private final class CraftNativeScrollView: UIScrollView, UIGestureRecognizerDele
|
|
|
5192
5192
|
/// 3 quadratic, 4 close, in viewBox units - and scaled to the view. Paint is
|
|
5193
5193
|
/// inherited down the tree as SVG does; `dark:` classes arrive already
|
|
5194
5194
|
/// resolved for the appearance, so a redraw is all a theme change needs.
|
|
5195
|
+
/// Native inspection of an SVG plot. Coordinates are fractions of the plot's
|
|
5196
|
+
/// bounds; labels and values are already localized by the application. No JS
|
|
5197
|
+
/// callback or bridge round trip is needed while a finger moves.
|
|
5198
|
+
final class CraftChartInspection: NSObject, UIGestureRecognizerDelegate {
|
|
5199
|
+
struct Reading {
|
|
5200
|
+
let key: String
|
|
5201
|
+
let x: CGFloat
|
|
5202
|
+
let y: CGFloat?
|
|
5203
|
+
let color: UIColor
|
|
5204
|
+
let label: String
|
|
5205
|
+
let rows: [(String, String, UIColor?)]
|
|
5206
|
+
var announcement: String { ([label] + rows.map { $0.0 + ": " + $0.1 }).joined(separator: ". ") }
|
|
5207
|
+
}
|
|
5208
|
+
private weak var view: UIView?
|
|
5209
|
+
private var configuration: NSDictionary = [:]
|
|
5210
|
+
private(set) var readings: [Reading] = []
|
|
5211
|
+
private(set) var selectedIndex = -1
|
|
5212
|
+
private var title = ""
|
|
5213
|
+
private let feedback = UISelectionFeedbackGenerator()
|
|
5214
|
+
private lazy var tap = UITapGestureRecognizer(target: self, action: #selector(tapped(_:)))
|
|
5215
|
+
private lazy var pan = UIPanGestureRecognizer(target: self, action: #selector(panned(_:)))
|
|
5216
|
+
private lazy var hold = UILongPressGestureRecognizer(target: self, action: #selector(held(_:)))
|
|
5217
|
+
|
|
5218
|
+
init(view: UIView) {
|
|
5219
|
+
self.view = view
|
|
5220
|
+
super.init()
|
|
5221
|
+
pan.delegate = self
|
|
5222
|
+
hold.minimumPressDuration = 0.25
|
|
5223
|
+
[tap, pan, hold].forEach { view.addGestureRecognizer($0); $0.isEnabled = false }
|
|
5224
|
+
}
|
|
5225
|
+
|
|
5226
|
+
func configure(_ raw: Any?) {
|
|
5227
|
+
let next = raw as? [String: Any] ?? [:]
|
|
5228
|
+
guard !configuration.isEqual(to: next) else { return }
|
|
5229
|
+
configuration = next as NSDictionary
|
|
5230
|
+
title = next["title"] as? String ?? ""
|
|
5231
|
+
readings = (next["points"] as? [[String: Any]] ?? []).compactMap { point in
|
|
5232
|
+
guard let x = craftNativeNumber(point["x"])?.doubleValue, x.isFinite, x >= 0, x <= 1,
|
|
5233
|
+
let label = point["label"] as? String else { return nil }
|
|
5234
|
+
let rawY = craftNativeNumber(point["y"])?.doubleValue
|
|
5235
|
+
let y = rawY.flatMap { $0.isFinite && $0 >= 0 && $0 <= 1 ? CGFloat($0) : nil }
|
|
5236
|
+
let rows = (point["rows"] as? [[String: Any]] ?? []).compactMap { row -> (String, String, UIColor?)? in
|
|
5237
|
+
guard let label = row["label"] as? String, let value = row["value"] as? String else { return nil }
|
|
5238
|
+
return (label, value, (row["color"] as? String).flatMap { UIColor(css: $0) })
|
|
5239
|
+
}
|
|
5240
|
+
return Reading(key: point["key"] as? String ?? label, x: CGFloat(x), y: y,
|
|
5241
|
+
color: (point["color"] as? String).flatMap { UIColor(css: $0) } ?? .systemBlue, label: label, rows: rows)
|
|
5242
|
+
}
|
|
5243
|
+
selectedIndex = -1
|
|
5244
|
+
view?.isUserInteractionEnabled = !readings.isEmpty
|
|
5245
|
+
if readings.isEmpty { view?.accessibilityTraits.remove(.adjustable); view?.accessibilityValue = nil }
|
|
5246
|
+
[tap, pan, hold].forEach { $0.isEnabled = !readings.isEmpty }
|
|
5247
|
+
configureAccessibility()
|
|
5248
|
+
view?.setNeedsDisplay()
|
|
5249
|
+
}
|
|
5250
|
+
|
|
5251
|
+
func configureAccessibility() {
|
|
5252
|
+
guard let view, !readings.isEmpty else { return }
|
|
5253
|
+
view.isAccessibilityElement = true
|
|
5254
|
+
view.accessibilityTraits.insert(.adjustable)
|
|
5255
|
+
view.accessibilityValue = readings[selectedIndex >= 0 ? selectedIndex : readings.count - 1].announcement
|
|
5256
|
+
}
|
|
5257
|
+
|
|
5258
|
+
func select(_ index: Int, haptic: Bool = true) {
|
|
5259
|
+
guard !readings.isEmpty else { return }
|
|
5260
|
+
let next = max(0, min(readings.count - 1, index))
|
|
5261
|
+
guard next != selectedIndex else { return }
|
|
5262
|
+
selectedIndex = next
|
|
5263
|
+
if haptic { feedback.selectionChanged() }
|
|
5264
|
+
configureAccessibility()
|
|
5265
|
+
view?.setNeedsDisplay()
|
|
5266
|
+
}
|
|
5267
|
+
|
|
5268
|
+
func adjust(_ direction: Int) {
|
|
5269
|
+
select(selectedIndex < 0 ? (direction < 0 ? readings.count - 1 : 0) : selectedIndex + direction)
|
|
5270
|
+
}
|
|
5271
|
+
|
|
5272
|
+
@discardableResult func dismiss() -> Bool {
|
|
5273
|
+
guard selectedIndex >= 0 else { return false }
|
|
5274
|
+
selectedIndex = -1
|
|
5275
|
+
configureAccessibility()
|
|
5276
|
+
view?.setNeedsDisplay()
|
|
5277
|
+
return true
|
|
5278
|
+
}
|
|
5279
|
+
|
|
5280
|
+
private func select(at point: CGPoint) {
|
|
5281
|
+
guard let view, view.bounds.width > 0 else { return }
|
|
5282
|
+
let fraction = max(0, min(1, point.x / view.bounds.width))
|
|
5283
|
+
if let nearest = readings.indices.min(by: { abs(readings[$0].x - fraction) < abs(readings[$1].x - fraction) }) { select(nearest) }
|
|
5284
|
+
}
|
|
5285
|
+
@objc private func tapped(_ gesture: UITapGestureRecognizer) { select(at: gesture.location(in: view)) }
|
|
5286
|
+
@objc private func panned(_ gesture: UIPanGestureRecognizer) {
|
|
5287
|
+
if gesture.state == .began || gesture.state == .changed { select(at: gesture.location(in: view)) }
|
|
5288
|
+
if gesture.state == .cancelled { dismiss() }
|
|
5289
|
+
}
|
|
5290
|
+
@objc private func held(_ gesture: UILongPressGestureRecognizer) {
|
|
5291
|
+
if gesture.state == .began || gesture.state == .changed { select(at: gesture.location(in: view)) }
|
|
5292
|
+
if gesture.state == .cancelled { dismiss() }
|
|
5293
|
+
}
|
|
5294
|
+
func gestureRecognizerShouldBegin(_ gesture: UIGestureRecognizer) -> Bool {
|
|
5295
|
+
guard gesture === pan else { return true }
|
|
5296
|
+
let velocity = pan.velocity(in: view)
|
|
5297
|
+
return abs(velocity.x) > abs(velocity.y)
|
|
5298
|
+
}
|
|
5299
|
+
func gestureRecognizer(_ gesture: UIGestureRecognizer, shouldBeRequiredToFailBy other: UIGestureRecognizer) -> Bool {
|
|
5300
|
+
gesture === pan && other.view is UIScrollView
|
|
5301
|
+
}
|
|
5302
|
+
|
|
5303
|
+
func draw(in context: CGContext, bounds: CGRect) {
|
|
5304
|
+
guard readings.indices.contains(selectedIndex), bounds.width > 24, bounds.height > 24 else { return }
|
|
5305
|
+
let reading = readings[selectedIndex], x = bounds.width * reading.x
|
|
5306
|
+
context.saveGState()
|
|
5307
|
+
defer { context.restoreGState() }
|
|
5308
|
+
context.setStrokeColor(UIColor.secondaryLabel.withAlphaComponent(0.65).cgColor)
|
|
5309
|
+
context.setLineWidth(1)
|
|
5310
|
+
context.setLineDash(phase: 0, lengths: [2, 3])
|
|
5311
|
+
context.move(to: CGPoint(x: x, y: 0)); context.addLine(to: CGPoint(x: x, y: bounds.height)); context.strokePath()
|
|
5312
|
+
context.setLineDash(phase: 0, lengths: [])
|
|
5313
|
+
if let y = reading.y {
|
|
5314
|
+
let dot = CGRect(x: x - 5, y: bounds.height * y - 5, width: 10, height: 10)
|
|
5315
|
+
context.setFillColor(reading.color.cgColor); context.fillEllipse(in: dot)
|
|
5316
|
+
context.setStrokeColor(UIColor.systemBackground.cgColor); context.setLineWidth(2); context.strokeEllipse(in: dot)
|
|
5317
|
+
}
|
|
5318
|
+
let font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 12), maximumPointSize: 20)
|
|
5319
|
+
let bold = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 12, weight: .semibold), maximumPointSize: 20)
|
|
5320
|
+
let rowHeight = ceil(font.lineHeight) + 7
|
|
5321
|
+
let width = min(264, bounds.width - 16)
|
|
5322
|
+
let height = min(bounds.height - 16, 24 + rowHeight * CGFloat(reading.rows.count + (title.isEmpty ? 1 : 2)))
|
|
5323
|
+
let left = max(8, min(bounds.width - width - 8, x > bounds.midX ? x - width - 12 : x + 12))
|
|
5324
|
+
let panel = CGRect(x: left, y: 8, width: width, height: height)
|
|
5325
|
+
context.setShadow(offset: CGSize(width: 0, height: 4), blur: 12, color: UIColor.black.withAlphaComponent(0.16).cgColor)
|
|
5326
|
+
UIColor.secondarySystemGroupedBackground.setFill(); UIBezierPath(roundedRect: panel, cornerRadius: 12).fill()
|
|
5327
|
+
context.setShadow(offset: .zero, blur: 0, color: nil)
|
|
5328
|
+
UIColor.separator.withAlphaComponent(0.5).setStroke(); let border = UIBezierPath(roundedRect: panel.insetBy(dx: 0.5, dy: 0.5), cornerRadius: 12); border.lineWidth = 1; border.stroke()
|
|
5329
|
+
context.clip(to: panel.insetBy(dx: 10, dy: 8))
|
|
5330
|
+
let paragraph = NSMutableParagraphStyle(); paragraph.lineBreakMode = .byTruncatingTail
|
|
5331
|
+
func text(_ value: String, rect: CGRect, strong: Bool = false, right: Bool = false, secondary: Bool = false) {
|
|
5332
|
+
paragraph.alignment = right ? .right : .left
|
|
5333
|
+
(value as NSString).draw(in: rect, withAttributes: [.font: strong ? bold : font, .foregroundColor: secondary ? UIColor.label.withAlphaComponent(0.72) : UIColor.label, .paragraphStyle: paragraph])
|
|
5334
|
+
}
|
|
5335
|
+
var top = panel.minY + 12
|
|
5336
|
+
if !title.isEmpty { text(title, rect: CGRect(x: left + 12, y: top, width: width - 24, height: rowHeight), strong: true); top += rowHeight }
|
|
5337
|
+
text(reading.label, rect: CGRect(x: left + 12, y: top, width: width - 24, height: rowHeight), secondary: true); top += rowHeight
|
|
5338
|
+
for row in reading.rows {
|
|
5339
|
+
var labelLeft = left + 12
|
|
5340
|
+
if let color = row.2 { color.setFill(); UIBezierPath(roundedRect: CGRect(x: labelLeft, y: top + 4, width: 8, height: 8), cornerRadius: 1).fill(); labelLeft += 14 }
|
|
5341
|
+
text(row.0, rect: CGRect(x: labelLeft, y: top, width: width * 0.47 - (labelLeft - left), height: rowHeight), secondary: true)
|
|
5342
|
+
text(row.1, rect: CGRect(x: left + width * 0.48, y: top, width: width * 0.52 - 12, height: rowHeight), strong: true, right: true)
|
|
5343
|
+
top += rowHeight
|
|
5344
|
+
}
|
|
5345
|
+
}
|
|
5346
|
+
}
|
|
5347
|
+
|
|
5195
5348
|
final class CraftNativeSvgView: UIView {
|
|
5349
|
+
lazy var inspection = CraftChartInspection(view: self)
|
|
5350
|
+
override func accessibilityIncrement() { inspection.adjust(1) }
|
|
5351
|
+
override func accessibilityDecrement() { inspection.adjust(-1) }
|
|
5352
|
+
override func accessibilityPerformEscape() -> Bool { inspection.dismiss() }
|
|
5196
5353
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
5197
5354
|
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
5198
5355
|
}
|
|
@@ -5200,6 +5357,7 @@ final class CraftNativeSvgView: UIView {
|
|
|
5200
5357
|
var drawing: [String: Any] = [:] {
|
|
5201
5358
|
didSet {
|
|
5202
5359
|
animates = Self.hasAnimation(drawing["children"] as? [Any] ?? [])
|
|
5360
|
+
inspection.configure((drawing["props"] as? [String: Any])?["inspection"])
|
|
5203
5361
|
setNeedsDisplay()
|
|
5204
5362
|
updateFrames()
|
|
5205
5363
|
}
|
|
@@ -5426,14 +5584,17 @@ final class CraftNativeSvgView: UIView {
|
|
|
5426
5584
|
|
|
5427
5585
|
override func draw(_ rect: CGRect) {
|
|
5428
5586
|
guard let context = UIGraphicsGetCurrentContext() else { return }
|
|
5587
|
+
context.saveGState()
|
|
5429
5588
|
viewTransform = .identity
|
|
5430
5589
|
if let box = viewBox {
|
|
5431
5590
|
let props = drawing["props"] as? [String: Any] ?? [:]
|
|
5432
|
-
guard let transform = CraftNativeSvgView.transform(box: box, bounds: bounds.size, preserveAspectRatio: props["preserveAspectRatio"] as? String) else { return }
|
|
5591
|
+
guard let transform = CraftNativeSvgView.transform(box: box, bounds: bounds.size, preserveAspectRatio: props["preserveAspectRatio"] as? String) else { context.restoreGState(); return }
|
|
5433
5592
|
viewTransform = transform
|
|
5434
5593
|
context.concatenate(transform)
|
|
5435
5594
|
}
|
|
5436
5595
|
draw(children: drawing["children"] as? [Any] ?? [], paint: Paint().merged(drawing), in: context)
|
|
5596
|
+
context.restoreGState()
|
|
5597
|
+
inspection.draw(in: context, bounds: bounds)
|
|
5437
5598
|
}
|
|
5438
5599
|
|
|
5439
5600
|
private func draw(children: [Any], paint: Paint, in context: CGContext) {
|
|
@@ -9832,6 +9993,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
9832
9993
|
}
|
|
9833
9994
|
result.craftPointerEvents = CraftNativePointerEvents.resolve(props: props, style: style)
|
|
9834
9995
|
applyAccessibility(props, type: type, to: result)
|
|
9996
|
+
(result as? CraftNativeSvgView)?.inspection.configureAccessibility()
|
|
9835
9997
|
// After applyAccessibility: it settles whether the view is an element
|
|
9836
9998
|
// at all, and an action on a non-element is unreachable.
|
|
9837
9999
|
updateAccessibilityActions(props["accessibilityActions"], handler: menuDisabled ? nil : events["onAccessibilityAction"], for: result)
|
|
@@ -10479,7 +10641,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
10479
10641
|
/// not be panned, zoomed, or have a pin tapped; a picker could not be spun.
|
|
10480
10642
|
private func ownsTouches(_ view: UIView) -> Bool {
|
|
10481
10643
|
view is UIControl || view is UIScrollView || view is CraftNativeModalView || view is CraftNativeVideoView
|
|
10482
|
-
|| view is MKMapView || view is UIPickerView
|
|
10644
|
+
|| view is MKMapView || view is UIPickerView || ((view as? CraftNativeSvgView)?.inspection.readings.isEmpty == false)
|
|
10483
10645
|
}
|
|
10484
10646
|
|
|
10485
10647
|
private func updatePressHandler(_ handler: String?, for view: UIView) {
|