@craft-native/ios 0.0.209 → 0.0.211

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 CHANGED
@@ -137,7 +137,13 @@ and linear gradients are applied by UIKit with the same style keys as Android;
137
137
  `elevation` remains Android-specific. Gradient layers follow the native view's
138
138
  bounds and corners through layout changes.
139
139
 
140
- The opt-in `native` renderer hosts compiled `.stx` screens in JavaScriptCore and renders their `View`, `SafeAreaView`, `Text`, `Button`, `Link`, `Checkbox`, `TextInput`, `Picker`, `DatePicker`, `Switch`, `Slider`, `ActivityIndicator`, `Icon`, `Image`, `ImageBackground`, `Video`, `Map`, `Svg`, `G`, `SvgText`, `ScrollView`, `SectionList`, and recycling `FlatList` nodes as UIKit controls. `Map` uses MapKit for trail polylines and marker taps; its overlays carry flat latitude/longitude `geometry` arrays, `strokeWidth` is expressed in logical points, and the camera supports controlled `region`, one-time `initialRegion`, and `fitToBounds`. `Svg` is a DOM-free drawing surface; grouped `G` and `SvgText` nodes are painted inside it. `Icon` uses SF Symbols with the shared symbol/name contract and accessibility metadata. `SectionList` uses the same native scroll host for ordinary `.stx` slot children; grouped section data should use `FlatList` until the section-data protocol is available. `FlatList` uses `UICollectionView` with stable keyed rows, incremental diffs, grids, horizontal and inverted layouts, list chrome, and end-reached pagination. Native views support `onLayout` callbacks with point-based frame measurements that refresh after safe-area or orientation changes. Flex items support `flex`, `flexGrow`, `flexShrink`, and numeric `flexBasis` for main-axis space distribution. Native screens also expose the versioned capability bridge: JSON storage and SQLite survive relaunch, lifecycle and deep-link events are scoped to the top route, and local notifications use `UNUserNotificationCenter`. Enable each capability in `craft.config.json`; disabled calls preserve Craft's `CAPABILITY_DISABLED` rejection code and every request has deterministic timeout/cancellation behavior. The default remains `web`; existing iOS apps are unchanged. `Checkbox` supports controlled `checked`/`value`, indeterminate state, and boolean `onValueChange`; `Picker` renders `<select>`/`<option>` values with `onValueChange` or `onChange`; `DatePicker` emits local date/time values plus ISO and millisecond timestamps; `Switch` supports state-aware `trackColor`, `thumbColor`, and iOS `ios_backgroundColor`; `Slider` supports track/thumb tint props and `step` snapping; `Image` reports load lifecycle events and structured failures; `Video` uses an `AVPlayerViewController` attached to the native screen for URI playback, native controls, and route-safe teardown. The portable component, capability, list, style, image-source, event, accessibility, and reconciliation behavior is specified in the [Native STX guide](../../docs/guides/native-stx.md).
140
+ The opt-in `native` renderer hosts compiled `.stx` screens in JavaScriptCore and renders their `View`, `SafeAreaView`, `KeyboardAvoidingView`, `StatusBar`, `RefreshControl`, `Text`, `Button`, `Link`, `Checkbox`, `TextInput`, `Picker`, `DatePicker`, `Switch`, `Slider`, `ActivityIndicator`, `Icon`, `Image`, `ImageBackground`, `Video`, `Map`, `Svg`, `G`, `SvgText`, `ScrollView`, `SectionList`, and recycling `FlatList` nodes as UIKit controls. `StatusBar` and `RefreshControl` are screen/scroll utility nodes rather than visible children. `Map` uses MapKit for trail polylines and marker taps; its overlays carry flat latitude/longitude `geometry` arrays, `strokeWidth` is expressed in logical points, and the camera supports controlled `region`, one-time `initialRegion`, and `fitToBounds`. `Svg` is a DOM-free drawing surface; grouped `G` and `SvgText` nodes are painted inside it. `Icon` uses SF Symbols with the shared symbol/name contract and accessibility metadata. `SectionList` uses the same native scroll host for ordinary `.stx` slot children; grouped section data should use `FlatList` until the section-data protocol is available. `FlatList` uses `UICollectionView` with stable keyed rows, incremental diffs, grids, horizontal and inverted layouts, list chrome, and end-reached pagination. Native views support `onLayout` callbacks with point-based frame measurements that refresh after safe-area or orientation changes. Flex items support `flex`, `flexGrow`, `flexShrink`, and numeric `flexBasis` for main-axis space distribution. Native screens also expose the versioned capability bridge: JSON storage and SQLite survive relaunch, lifecycle and deep-link events are scoped to the top route, and local notifications use `UNUserNotificationCenter`. Enable each capability in `craft.config.json`; disabled calls preserve Craft's `CAPABILITY_DISABLED` rejection code and every request has deterministic timeout/cancellation behavior. The default remains `web`; existing iOS apps are unchanged. `Checkbox` supports controlled `checked`/`value`, indeterminate state, and boolean `onValueChange`; `Picker` renders `<select>`/`<option>` values with `onValueChange` or `onChange`; `DatePicker` emits local date/time values plus ISO and millisecond `timestamp` fields; `Switch` supports state-aware `trackColor`, `thumbColor`, and iOS `ios_backgroundColor`; `Slider` supports track/thumb tint props and `step` snapping; `Image` reports load lifecycle events and structured failures; `Video` uses an `AVPlayerViewController` attached to the native screen for URI playback, native controls, and route-safe teardown. `craft.navigation.setOptions` also accepts a `tabAccessory` destination beside the tab pill; it uses an app-local `path`, `title`, `symbol`, optional `badge`, and `accessibilityLabel`, and `null` clears it. The portable component, capability, list, style, image-source, event, accessibility, and reconciliation behavior is specified in the [Native STX guide](../../docs/guides/native-stx.md).
141
+
142
+ JavaScriptCore has no DOM `window`; Craft aliases `globalThis.window` to
143
+ `globalThis` before installing the native bridge so browser-safe SDK calls such
144
+ as `window.craft.dialog.actionSheet()` reach the native capability host. This
145
+ does not add `document` or DOM rendering; routes that do not opt into `native`
146
+ continue using the WebView renderer.
141
147
 
142
148
  ```bash
143
149
  craft ios init NativeDemo --renderer native --output ./ios-native
@@ -181,6 +187,10 @@ under test is activated by its bundle identifier and is not rebuilt by the
181
187
  driver. Export evidence with `xcrun xcresulttool export attachments --path
182
188
  <result.xcresult> --output-path <evidence>`. Omitting `script` restores the
183
189
  control-server protocol, required for the `net`, `delay` and `exec` hooks.
190
+ Journeys stop at the first failed check so later actions cannot operate on the
191
+ wrong screen. `systemtap` targets the system button around a Live Activity rather
192
+ than its remotely hosted text. The [Live Activity layout fixture](./fixtures/live-activity)
193
+ exercises long names and timer alignment using the production widget.
184
194
 
185
195
  The iOS host uses the shared tree-layout ABI for eligible definite-size,
186
196
  non-grid, non-wrapped containers and retains a local fallback for older
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@craft-native/ios",
3
- "version": "0.0.209",
3
+ "version": "0.0.211",
4
4
  "homepage": "https://github.com/craft-native/craft/tree/main/packages/ios#readme",
5
5
  "repository": {
6
6
  "type": "git",
@@ -928,10 +928,12 @@ struct CraftTabBarView: View {
928
928
  bar
929
929
  if let accessory = chrome.tabAccessory {
930
930
  Button { CraftHybrid.shared.open(accessory.id) } label: {
931
- Image(systemName: accessory.symbol)
932
- .font(.system(size: iconSize))
933
- .frame(width: 62, height: 62)
934
- .background(.ultraThinMaterial, in: Circle())
931
+ surface {
932
+ Image(systemName: accessory.symbol)
933
+ .font(.system(size: iconSize))
934
+ .foregroundStyle(Color.primary)
935
+ .frame(width: 62, height: 62)
936
+ }
935
937
  .overlay(alignment: .topTrailing) {
936
938
  if let badge = accessory.badge {
937
939
  Text(badge)
@@ -967,24 +969,27 @@ struct CraftTabBarView: View {
967
969
  chrome.layoutChanged(max(0, screen - proxy.frame(in: .global).minY))
968
970
  }
969
971
 
970
- @ViewBuilder private var bar: some View {
972
+ private var bar: some View {
973
+ surface { items.padding(4) }
974
+ }
975
+
976
+ /// Both the tab capsule and its circular accessory use the same material,
977
+ /// outline and shadow. A square capsule forms the accessory's circle.
978
+ @ViewBuilder private func surface<Content: View>(@ViewBuilder content: () -> Content) -> some View {
971
979
  #if compiler(>=6.2)
972
980
  if #available(iOS 26.0, *) {
973
981
  GlassEffectContainer {
974
- items
975
- .padding(4)
982
+ content()
976
983
  .glassEffect(.regular.interactive(), in: .capsule)
977
984
  }
978
985
  } else {
979
- items
980
- .padding(4)
986
+ content()
981
987
  .background(.ultraThinMaterial, in: Capsule())
982
988
  .overlay(Capsule().strokeBorder(Color.white.opacity(0.35), lineWidth: 0.5))
983
989
  .shadow(color: .black.opacity(0.12), radius: 18, y: 6)
984
990
  }
985
991
  #else
986
- items
987
- .padding(4)
992
+ content()
988
993
  .background(.ultraThinMaterial, in: Capsule())
989
994
  .overlay(Capsule().strokeBorder(Color.white.opacity(0.35), lineWidth: 0.5))
990
995
  .shadow(color: .black.opacity(0.12), radius: 18, y: 6)
@@ -11,9 +11,9 @@ struct CraftLiveActivityWidget: Widget {
11
11
  var body: some WidgetConfiguration {
12
12
  ActivityConfiguration(for: CraftActivityAttributes.self) { context in
13
13
  HStack(spacing: 12) {
14
- Image(systemName: symbol(context)).font(.title2).foregroundStyle(tint(context))
14
+ activityIcon(context).font(.title2)
15
15
  VStack(alignment: .leading, spacing: 4) {
16
- Text(context.attributes.title).font(.headline).lineLimit(1)
16
+ Text(context.attributes.title).font(.headline).lineLimit(2)
17
17
  if let detail = context.state.detail, !detail.isEmpty {
18
18
  Text(detail).font(.subheadline).lineLimit(1)
19
19
  }
@@ -33,14 +33,14 @@ struct CraftLiveActivityWidget: Widget {
33
33
  } dynamicIsland: { context in
34
34
  DynamicIsland {
35
35
  DynamicIslandExpandedRegion(.leading) {
36
- Image(systemName: symbol(context)).foregroundStyle(tint(context)).font(.title2)
36
+ activityIcon(context).font(.title2)
37
37
  }
38
38
  DynamicIslandExpandedRegion(.trailing) {
39
39
  timer(context).font(.title2.monospacedDigit().weight(.semibold))
40
40
  }
41
41
  DynamicIslandExpandedRegion(.center) {
42
42
  VStack(spacing: 3) {
43
- Text(context.attributes.title).font(.headline).lineLimit(1)
43
+ Text(context.attributes.title).font(.headline).lineLimit(2)
44
44
  Text(context.isStale ? context.state.staleStatus ?? context.state.status : context.state.status).font(.caption).foregroundStyle(.secondary).lineLimit(1)
45
45
  }
46
46
  }
@@ -59,17 +59,26 @@ struct CraftLiveActivityWidget: Widget {
59
59
  }
60
60
  }
61
61
  } compactLeading: {
62
- Image(systemName: symbol(context)).foregroundStyle(tint(context))
62
+ activityIcon(context)
63
63
  } compactTrailing: {
64
- timer(context).monospacedDigit().frame(maxWidth: 64)
64
+ timer(context).monospacedDigit().frame(maxWidth: 64, alignment: .trailing)
65
65
  } minimal: {
66
- Image(systemName: symbol(context)).foregroundStyle(tint(context))
66
+ activityIcon(context)
67
67
  }
68
68
  .widgetURL(context.attributes.deepLink.flatMap(URL.init(string:)))
69
69
  .keylineTint(tint(context))
70
70
  }
71
71
  }
72
72
 
73
+ private func activityIcon(_ context: ActivityViewContext<CraftActivityAttributes>) -> some View {
74
+ // The pause glyph uses the active symbol's footprint, keeping the
75
+ // title's leading edge steady through state changes.
76
+ Image(systemName: context.attributes.symbol ?? "timer")
77
+ .hidden()
78
+ .overlay { Image(systemName: symbol(context)) }
79
+ .foregroundStyle(tint(context))
80
+ }
81
+
73
82
  private func symbol(_ context: ActivityViewContext<CraftActivityAttributes>) -> String {
74
83
  context.state.paused == true ? "pause.fill" : context.attributes.symbol ?? "timer"
75
84
  }
@@ -82,8 +91,23 @@ struct CraftLiveActivityWidget: Widget {
82
91
 
83
92
  private func date(_ milliseconds: Double) -> Date { Date(timeIntervalSince1970: milliseconds / 1000) }
84
93
 
85
- @ViewBuilder
86
94
  private func timer(_ context: ActivityViewContext<CraftActivityAttributes>) -> some View {
95
+ // Measure the hour format in the inherited native font. A widget's
96
+ // ticking Text is horizontally flexible; an overlay bounds it to this
97
+ // footprint so names retain the same width when the timer pauses.
98
+ Text(duration(max(3600, context.state.durationSeconds, context.state.remainingSeconds ?? 0)))
99
+ .lineLimit(1)
100
+ .hidden()
101
+ .overlay(alignment: .trailing) {
102
+ timerContent(context)
103
+ .multilineTextAlignment(.trailing)
104
+ .lineLimit(1)
105
+ .minimumScaleFactor(0.8)
106
+ }
107
+ }
108
+
109
+ @ViewBuilder
110
+ private func timerContent(_ context: ActivityViewContext<CraftActivityAttributes>) -> some View {
87
111
  if context.state.paused == true {
88
112
  Text(duration(context.state.remainingSeconds ?? context.state.durationSeconds))
89
113
  } else if let end = context.state.timerEndsAt {
@@ -4151,6 +4151,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
4151
4151
  let appVersion = Self.jsonString(info?["CFBundleShortVersionString"] as? String ?? "")
4152
4152
  let appBuild = Self.jsonString(info?["CFBundleVersion"] as? String ?? "")
4153
4153
  jsContext.evaluateScript("""
4154
+ // JavaScriptCore has no DOM window, but browser-safe SDK bridges
4155
+ // address the host through `window.craft`. Keep native screens
4156
+ // compatible without pretending a document exists.
4157
+ if (typeof globalThis.window === "undefined") { globalThis.window = globalThis; }
4154
4158
  globalThis.__stxNativeCallback = null;
4155
4159
  globalThis.__stxNativeBridge = {
4156
4160
  platform: "ios",
@@ -4279,7 +4283,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
4279
4283
  /// `craft.secureStorage.set` wrote.
4280
4284
  /// - `craft.navigation.setOptions({ title, largeTitle, hidden, backTitle,
4281
4285
  /// tabBarHidden, swipeBack, contentSwipeBack, tabSwipeEnabled,
4282
- /// backgroundColor, navigationBarSolid, rightButtons })`;
4286
+ /// backgroundColor, navigationBarSolid, rightButtons, tabAccessory })`;
4283
4287
  /// a right button's tap is the event `navButton`.
4284
4288
  ///
4285
4289
  /// A runtime that assigns `craft.storage` (or the others) afterwards keeps
@@ -5731,7 +5735,6 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
5731
5735
  ? nil : number(props["activeOpacity"]) ?? 0.5
5732
5736
  pressable.pressBaseOpacity = number(style["opacity"]) ?? 1
5733
5737
  pressable.pressUnderlayColor = type == "TouchableHighlight" && !disabled ? color(props["underlayColor"]) : nil
5734
- pressable.pressBaseBackgroundColor = pressable.backgroundColor
5735
5738
  case "Switch":
5736
5739
  let toggle = result as! UISwitch
5737
5740
  toggle.isOn = props["value"] as? Bool ?? props["checked"] as? Bool ?? false
@@ -6689,6 +6692,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
6689
6692
  updateDimension(width, constraint: &node.widthConstraint, anchor: view.widthAnchor)
6690
6693
  updateDimension(height, constraint: &node.heightConstraint, anchor: view.heightAnchor)
6691
6694
  view.backgroundColor = color(style["backgroundColor"]) ?? .clear
6695
+ // Every flow container can receive press feedback, including ordinary
6696
+ // Views. Capture its current authored fill before feedback cleanup or
6697
+ // release restores it; otherwise avatars and button wrappers go clear.
6698
+ (view as? CraftNativeFlowView)?.pressBaseBackgroundColor = view.backgroundColor
6692
6699
  // A field the style dresses itself (`border-0`, a border or a
6693
6700
  // background, as a search field inside its own pill is) draws no
6694
6701
  // system box of its own; a bare one keeps the rounded rectangle.
@@ -6940,7 +6947,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
6940
6947
  private func textFont(_ style: [String: Any], default fallback: UIFont) -> UIFont {
6941
6948
  let size = number(style["fontSize"]) ?? fallback.pointSize
6942
6949
  let weight: UIFont.Weight
6943
- switch style["fontWeight"] as? String {
6950
+ switch craftNativeFontWeight(style["fontWeight"]) {
6944
6951
  case "100": weight = .ultraLight
6945
6952
  case "200": weight = .thin
6946
6953
  case "300": weight = .light
@@ -6962,7 +6969,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
6962
6969
  ?? .systemFont(ofSize: scaledSize, weight: weight)
6963
6970
  if style["fontStyle"] as? String == "italic",
6964
6971
  let descriptor = font.fontDescriptor.withSymbolicTraits(.traitItalic) {
6965
- font = UIFont(descriptor: descriptor, size: size)
6972
+ font = UIFont(descriptor: descriptor, size: scaledSize)
6966
6973
  }
6967
6974
  return font
6968
6975
  }
@@ -7162,7 +7169,16 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
7162
7169
  || controlledVideo
7163
7170
  view.isAccessibilityElement = !container && role != "none" && (view.accessibilityLabel != nil || role != nil || ["Text", "Button", "Link", "Checkbox", "Image", "ImageBackground", "Video", "TextInput", "Picker", "DatePicker", "Switch", "Slider"].contains(type))
7164
7171
  view.accessibilityContainerType = container && (view.accessibilityLabel != nil || controlledVideo) ? .semanticGroup : .none
7172
+ // Keep the interaction trait native controls expose. Rebuilding this
7173
+ // set from the STX role alone otherwise turns a Slider, Picker,
7174
+ // DatePicker, or Switch into a static VoiceOver item whenever the
7175
+ // screen omits redundant accessibility metadata.
7165
7176
  var traits: UIAccessibilityTraits = []
7177
+ switch type {
7178
+ case "Slider", "Picker", "DatePicker": traits.insert(.adjustable)
7179
+ case "Switch": traits.insert(.button)
7180
+ default: break
7181
+ }
7166
7182
  switch role ?? type.lowercased() {
7167
7183
  case "tablist": traits.insert(.tabBar)
7168
7184
  case "button", "checkbox": traits.insert(.button)
@@ -167,7 +167,7 @@ final class DriverTests: XCTestCase {
167
167
  }
168
168
 
169
169
  func testScript() throws {
170
- continueAfterFailure = true
170
+ continueAfterFailure = false
171
171
  let data: Data?
172
172
  if let resource = Bundle(for: DriverTests.self).url(forResource: "journey", withExtension: "txt") {
173
173
  bundledJourney = true
@@ -219,7 +219,13 @@ final class DriverTests: XCTestCase {
219
219
  check(element(containing: arg, in: springboard) == nil, "systemabsent \(arg)")
220
220
  case "systemtap":
221
221
  let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
222
- if let target = poll(10, { element(containing: arg, in: springboard) }) { press(target); note("systemtap \(arg): ok") }
222
+ // Widget text lives in a remote accessibility host. Its frame can
223
+ // be visible while XCTest cannot resolve an activation point. Tap
224
+ // the containing system button (the actual Live Activity card).
225
+ let predicate = NSPredicate(format: "label CONTAINS[c] %@ OR identifier == %@", arg, arg)
226
+ let card = springboard.buttons.containing(predicate).firstMatch
227
+ if card.exists && card.isHittable { press(card); note("systemtap \(arg): ok") }
228
+ else if let target = poll(10, { element(containing: arg, in: springboard) }) { press(target); note("systemtap \(arg): ok") }
223
229
  else { XCTFail("System element not found"); note("FAIL systemtap \(arg): NOT FOUND") }
224
230
  case "systemtree":
225
231
  let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")