@craft-native/ios 0.0.395 → 0.0.408

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.
@@ -517,6 +517,95 @@ enum CraftSymbols {
517
517
  /// controller, which offers no way to say which style it wants, so the first
518
518
  /// call teaches that controller's class to answer from here. Only that exact
519
519
  /// class is touched, and only once.
520
+ /// The app's typefaces on UIKit's own chrome. A screen's text follows the
521
+ /// stx tree's `fontFamily`; the navigation bar, the tab bar and bar button
522
+ /// labels are UIKit's, outside that tree, so a branded app drew them in the
523
+ /// system face beside screens in its own. `fontFamily` / `headingFontFamily`
524
+ /// (CSS stacks, as the screens use) put the same faces there, through the
525
+ /// appearance proxies every bar starts from.
526
+ enum CraftChromeFonts {
527
+ /// The packaged family a body or heading stack resolved to; nil keeps the system face.
528
+ private(set) static var body: String?
529
+ private(set) static var heading: String?
530
+
531
+ /// The first family of a CSS stack the app can draw. A generic family
532
+ /// ahead of any packaged one means the system face: nil.
533
+ static func family(_ stack: String?) -> String? {
534
+ guard let stack else { return nil }
535
+ for part in stack.split(separator: ",") {
536
+ let name = part.trimmingCharacters(in: .whitespacesAndNewlines).trimmingCharacters(in: CharacterSet(charactersIn: "\"'"))
537
+ switch name.lowercased() {
538
+ case "": continue
539
+ case "sans-serif", "ui-sans-serif", "system-ui", "-apple-system", "serif", "ui-serif", "monospace", "ui-monospace": return nil
540
+ default: if UIFont(name: name, size: 12) != nil { return name }
541
+ }
542
+ }
543
+ return nil
544
+ }
545
+
546
+ /// `family` at a chrome size, bold for a heavy weight and scaled with Dynamic Type.
547
+ static func font(_ family: String?, size: CGFloat, weight: UIFont.Weight, textStyle: UIFont.TextStyle) -> UIFont? {
548
+ guard let family, let custom = UIFont(name: family, size: size) else { return nil }
549
+ var traits = custom.fontDescriptor.symbolicTraits
550
+ if weight >= .semibold { traits.insert(.traitBold) }
551
+ let descriptor = custom.fontDescriptor.withSymbolicTraits(traits) ?? custom.fontDescriptor
552
+ return UIFontMetrics(forTextStyle: textStyle).scaledFont(for: UIFont(descriptor: descriptor, size: size))
553
+ }
554
+
555
+ static var titleFont: UIFont? { font(heading, size: 17, weight: .semibold, textStyle: .headline) }
556
+ static var largeTitleFont: UIFont? { font(heading, size: 34, weight: .bold, textStyle: .largeTitle) }
557
+ static var tabLabelFont: UIFont? { font(body, size: 10, weight: .medium, textStyle: .caption2) }
558
+ static var barButtonFont: UIFont? { font(body, size: 17, weight: .regular, textStyle: .body) }
559
+
560
+ static func configure(_ config: CraftConfig) {
561
+ body = family(config.fontFamily)
562
+ heading = family(config.headingFontFamily) ?? body
563
+ if let title = titleFont, let large = largeTitleFont {
564
+ let bar = UINavigationBar.appearance()
565
+ bar.titleTextAttributes = [.font: title]
566
+ bar.largeTitleTextAttributes = [.font: large]
567
+ }
568
+ if let label = tabLabelFont {
569
+ for state in [UIControl.State.normal, .selected] {
570
+ UITabBarItem.appearance().setTitleTextAttributes([.font: label], for: state)
571
+ }
572
+ }
573
+ if let button = barButtonFont {
574
+ for state in [UIControl.State.normal, .highlighted, .disabled] {
575
+ UIBarButtonItem.appearance().setTitleTextAttributes([.font: button], for: state)
576
+ }
577
+ }
578
+ }
579
+
580
+ /// The label face on a tab bar. The Liquid Glass bar reads its labels from
581
+ /// its own appearance and ignores UITabBarItem's proxy, so the face goes on
582
+ /// a copy of the bar's appearance, which keeps its material untouched.
583
+ static func apply(to tabBar: UITabBar) {
584
+ guard let label = tabLabelFont else { return }
585
+ func labelled(_ appearance: UITabBarAppearance) -> UITabBarAppearance {
586
+ let copy = appearance.copy()
587
+ for layout in [copy.stackedLayoutAppearance, copy.inlineLayoutAppearance, copy.compactInlineLayoutAppearance] {
588
+ for state in [layout.normal, layout.selected, layout.focused, layout.disabled] {
589
+ state.titleTextAttributes[.font] = label
590
+ }
591
+ }
592
+ return copy
593
+ }
594
+ tabBar.standardAppearance = labelled(tabBar.standardAppearance)
595
+ if let edge = tabBar.scrollEdgeAppearance { tabBar.scrollEdgeAppearance = labelled(edge) }
596
+ }
597
+
598
+ /// The title faces on a bar appearance a screen builds itself (an opaque bar).
599
+ static func apply(to appearance: UINavigationBarAppearance) {
600
+ if let title = titleFont { appearance.titleTextAttributes[.font] = title }
601
+ if let large = largeTitleFont { appearance.largeTitleTextAttributes[.font] = large }
602
+ if let button = barButtonFont {
603
+ appearance.buttonAppearance.normal.titleTextAttributes[.font] = button
604
+ appearance.doneButtonAppearance.normal.titleTextAttributes[.font] = button
605
+ }
606
+ }
607
+ }
608
+
520
609
  enum CraftStatusBar {
521
610
  static var style: UIStatusBarStyle = .default
522
611
  private static var taught = Set<ObjectIdentifier>()
@@ -17,7 +17,7 @@ import Darwin
17
17
  /// openurl <url> opens a link through the system, answering "Open in the app?"
18
18
  /// universallink <https-url> waits up to 60s for an OS-verified link; never opens a browser
19
19
  /// shot <name> screenshot → shots/<name>.png
20
- /// tree <name> accessibility tree → shots/<name>.txt
20
+ /// tree <name> accessibility tree + identified snapshot values → shots/<name>.txt / <name>-values.json
21
21
  /// tap <text> first hittable element whose label contains text
22
22
  /// tapif <text> tap it when it is on screen, else carry on (the big timer's "Show the controls")
23
23
  /// tapid <identifier>
@@ -367,6 +367,22 @@ final class DriverTests: XCTestCase {
367
367
  post("/save?name=\(arg).png", shot.pngRepresentation)
368
368
  case "tree":
369
369
  post("/save?name=\(arg).txt", Data(app.debugDescription.utf8))
370
+ // debugDescription abbreviates values further than the snapshot.
371
+ // Retain snapshot values separately; long receipts chunk themselves.
372
+ do {
373
+ var values: [[String: String]] = []
374
+ func collect(_ node: XCUIElementSnapshot) {
375
+ if !node.identifier.isEmpty, let value = node.value as? String {
376
+ values.append(["id": node.identifier, "label": node.label, "value": value])
377
+ }
378
+ for child in node.children { collect(child) }
379
+ }
380
+ collect(try app.snapshot())
381
+ let data = try JSONSerialization.data(withJSONObject: values, options: [.sortedKeys])
382
+ post("/save?name=\(arg)-values.json", data)
383
+ } catch {
384
+ XCTFail("Could not retain accessibility values: \(error)")
385
+ }
370
386
  case "tap":
371
387
  if let target = waitFor(arg, 10) { press(target); note("tap \(arg): ok") }
372
388
  else { XCTFail("Element not found"); note("FAIL tap \(arg): NOT FOUND") }