@craft-native/ios 0.0.500 → 0.0.503
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 +1 -1
- package/templates/CraftNativeScreen.swift +123 -37
package/package.json
CHANGED
|
@@ -214,31 +214,34 @@ private func craftNativePickerValue(_ value: Any?) -> String? {
|
|
|
214
214
|
return nil
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
217
|
+
/// The most tracks a grid is given, either way. A count is whatever the
|
|
218
|
+
/// script said (`gridTemplateRows: Number.MAX_VALUE`,
|
|
219
|
+
/// `repeat(4294967296, 1fr)`), and the layout makes an array of rows that
|
|
220
|
+
/// long and hands the shared engine both counts as a `UInt32`: the first ran
|
|
221
|
+
/// the process out of memory and the second trapped. No screen has room to
|
|
222
|
+
/// show more tracks than this.
|
|
223
|
+
let craftNativeGridTrackLimit = 1024
|
|
224
|
+
|
|
225
|
+
func craftNativeGridTrackCount(_ value: Any?) -> Int? {
|
|
226
|
+
if let number = craftNativeNumber(value) {
|
|
227
|
+
return min(craftNativeGridTrackLimit, max(1, craftNativeInt(number.doubleValue)))
|
|
224
228
|
}
|
|
225
|
-
let tracks = trimmed.split(whereSeparator: { $0 == " " || $0 == "\t" || $0 == "\n" })
|
|
226
|
-
return max(1, tracks.count)
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
private func craftNativeGridRowCount(_ value: Any?) -> Int? {
|
|
230
|
-
if let number = craftNativeNumber(value) { return max(1, number.intValue) }
|
|
231
229
|
guard let text = value as? String else { return nil }
|
|
232
230
|
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
233
231
|
guard !trimmed.isEmpty else { return nil }
|
|
234
232
|
if trimmed.hasPrefix("repeat(") {
|
|
235
|
-
let digits = trimmed.dropFirst("repeat(".count).prefix { $0
|
|
236
|
-
|
|
233
|
+
let digits = trimmed.dropFirst("repeat(".count).prefix { ("0"..."9").contains($0) }
|
|
234
|
+
// Digits past what an Int holds are still a count, and still too many.
|
|
235
|
+
if !digits.isEmpty { return min(craftNativeGridTrackLimit, max(1, Int(digits) ?? craftNativeGridTrackLimit)) }
|
|
237
236
|
}
|
|
238
237
|
let tracks = trimmed.split(whereSeparator: { $0 == " " || $0 == "\t" || $0 == "\n" })
|
|
239
|
-
return tracks.isEmpty ? nil :
|
|
238
|
+
return tracks.isEmpty ? nil : min(craftNativeGridTrackLimit, tracks.count)
|
|
240
239
|
}
|
|
241
240
|
|
|
241
|
+
private func craftNativeGridColumnCount(_ value: Any?) -> Int { craftNativeGridTrackCount(value) ?? 1 }
|
|
242
|
+
|
|
243
|
+
private func craftNativeGridRowCount(_ value: Any?) -> Int? { craftNativeGridTrackCount(value) }
|
|
244
|
+
|
|
242
245
|
/// Optional bridge to the shared Zig flex rule. Native hosts retain their
|
|
243
246
|
/// local implementation when no Craft runtime is linked (the normal SDK
|
|
244
247
|
/// fallback), while runtime-backed apps get one cross-platform calculation.
|
|
@@ -4376,7 +4379,11 @@ class CraftNativePositionedImageView: UIView {
|
|
|
4376
4379
|
}
|
|
4377
4380
|
let rawFit = value("objectFit"), rawPosition = value("objectPosition")
|
|
4378
4381
|
if rawFit != nil || rawPosition != nil {
|
|
4379
|
-
|
|
4382
|
+
// A position does not choose a fit, as in CSS. STX compiles
|
|
4383
|
+
// `object-cover` to `resizeMode: "cover"` and sets no objectFit,
|
|
4384
|
+
// so a cover image given a position was drawn as "fill":
|
|
4385
|
+
// stretched to its box instead of cropped (#532).
|
|
4386
|
+
let requested = rawFit as? String ?? Self.objectFit(resizeMode: style["resizeMode"] as? String ?? props["resizeMode"] as? String)
|
|
4380
4387
|
fit = ["fill", "contain", "cover", "none", "scale-down"].contains(requested) ? requested : "fill"
|
|
4381
4388
|
position = Self.objectPosition(rawPosition)
|
|
4382
4389
|
} else { fit = nil }
|
|
@@ -4404,6 +4411,17 @@ class CraftNativePositionedImageView: UIView {
|
|
|
4404
4411
|
var usesObjectGeometry: Bool { fit != nil }
|
|
4405
4412
|
var objectCovers: Bool { fit == "cover" }
|
|
4406
4413
|
|
|
4414
|
+
/// The fit a `resizeMode` already asks for, for an image that has a
|
|
4415
|
+
/// position and no `objectFit`; "fill" when it has neither.
|
|
4416
|
+
static func objectFit(resizeMode: String?) -> String {
|
|
4417
|
+
switch resizeMode {
|
|
4418
|
+
case "cover": return "cover"
|
|
4419
|
+
case "contain": return "contain"
|
|
4420
|
+
case "center": return "none"
|
|
4421
|
+
default: return "fill"
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
4424
|
+
|
|
4407
4425
|
private static func objectPosition(_ raw: Any?) -> CGPoint {
|
|
4408
4426
|
guard let text = raw as? String else { return CGPoint(x: 0.5, y: 0.5) }
|
|
4409
4427
|
var parts = text.lowercased().split(whereSeparator: { $0.isWhitespace }).map(String.init)
|
|
@@ -7184,6 +7202,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7184
7202
|
// a screen that hid its own.
|
|
7185
7203
|
navigationController?.setNavigationBarHidden(false, animated: animated)
|
|
7186
7204
|
}
|
|
7205
|
+
// Options can arrive before the screen is in a stack: whether it has
|
|
7206
|
+
// a screen to go back to is known now.
|
|
7207
|
+
if backButtonSpec != nil { applyLeadingBarItems() }
|
|
7187
7208
|
applyNavigationBarAppearance()
|
|
7188
7209
|
// Each screen says whether it wants the tab bar, so one that hid it
|
|
7189
7210
|
// never leaves it hidden for the next. Only the screen on screen:
|
|
@@ -7858,7 +7879,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7858
7879
|
/// - `craft.secureStorage.getSync(key)`: the Keychain item the web page's
|
|
7859
7880
|
/// `craft.secureStorage.set` wrote.
|
|
7860
7881
|
/// - `craft.navigation.setOptions({ title, titleColor, largeTitle, hidden,
|
|
7861
|
-
/// backTitle, tabBarHidden, tabBarHiddenWhenPushed, tabItem, swipeBack,
|
|
7882
|
+
/// backTitle, backButton, tabBarHidden, tabBarHiddenWhenPushed, tabItem, swipeBack,
|
|
7862
7883
|
/// contentSwipeBack,
|
|
7863
7884
|
/// tabSwipeEnabled, backgroundColor, navigationBarSolid, leftButtons,
|
|
7864
7885
|
/// rightButtons, tabAccessory })`; a button's tap is the event
|
|
@@ -8474,10 +8495,16 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
8474
8495
|
navigationController?.setNavigationBarHidden(hidden, animated: animatesNavigationBarChanges)
|
|
8475
8496
|
}
|
|
8476
8497
|
}
|
|
8498
|
+
if options.keys.contains("backButton") {
|
|
8499
|
+
backButtonSpec = options["backButton"] as? [String: Any]
|
|
8500
|
+
backButtonItem = nil
|
|
8501
|
+
}
|
|
8477
8502
|
if options.keys.contains("leftButtons") {
|
|
8478
8503
|
let buttons = (options["leftButtons"] as? [[String: Any]]) ?? []
|
|
8479
|
-
|
|
8480
|
-
|
|
8504
|
+
leftButtonItems = buttons.compactMap(navigationButton)
|
|
8505
|
+
}
|
|
8506
|
+
if options.keys.contains("backButton") || options.keys.contains("leftButtons") {
|
|
8507
|
+
applyLeadingBarItems()
|
|
8481
8508
|
}
|
|
8482
8509
|
if options.keys.contains("rightButtons") {
|
|
8483
8510
|
let buttons = (options["rightButtons"] as? [[String: Any]]) ?? []
|
|
@@ -8575,6 +8602,44 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
8575
8602
|
return actions.isEmpty ? nil : UIMenu(children: actions)
|
|
8576
8603
|
}
|
|
8577
8604
|
|
|
8605
|
+
/// `backButton`: what this screen's Back is drawn with, when it is not
|
|
8606
|
+
/// the chevron (`{ symbol: "xmark", accessibilityLabel: "Close" }` for a
|
|
8607
|
+
/// screen that closes rather than goes back). `leftButtons` are added
|
|
8608
|
+
/// beside Back, so a screen could not put its own symbol in Back's place
|
|
8609
|
+
/// (#527). Nil is the system's chevron.
|
|
8610
|
+
private var backButtonSpec: [String: Any]?
|
|
8611
|
+
private var backButtonItem: UIBarButtonItem?
|
|
8612
|
+
private var leftButtonItems: [UIBarButtonItem] = []
|
|
8613
|
+
|
|
8614
|
+
/// The screen's own Back, when it asked for one and has a screen to go
|
|
8615
|
+
/// back to, then its left buttons. The chevron is hidden only while the
|
|
8616
|
+
/// item that replaces it is in the bar: a symbol UIKit does not have
|
|
8617
|
+
/// leaves the chevron. The back-swipe is the shell's to allow, and is
|
|
8618
|
+
/// not affected.
|
|
8619
|
+
private func applyLeadingBarItems() {
|
|
8620
|
+
// Known to be the first screen of its stack: nothing to go back to.
|
|
8621
|
+
let isRoot = navigationController?.viewControllers.first === self
|
|
8622
|
+
if !isRoot, backButtonItem == nil, let spec = backButtonSpec,
|
|
8623
|
+
let symbol = spec["symbol"] as? String, let image = UIImage(systemName: symbol) {
|
|
8624
|
+
let item = UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(backButtonTapped))
|
|
8625
|
+
item.accessibilityIdentifier = "nav-back"
|
|
8626
|
+
item.accessibilityLabel = (spec["accessibilityLabel"] as? String).flatMap { $0.isEmpty ? nil : $0 }
|
|
8627
|
+
?? (symbol.hasPrefix("xmark") ? "Close" : "Back")
|
|
8628
|
+
backButtonItem = item
|
|
8629
|
+
}
|
|
8630
|
+
let back = isRoot ? nil : backButtonItem
|
|
8631
|
+
navigationItem.hidesBackButton = back != nil
|
|
8632
|
+
navigationItem.leftItemsSupplementBackButton = true
|
|
8633
|
+
navigationItem.leftBarButtonItems = (back.map { [$0] } ?? []) + leftButtonItems
|
|
8634
|
+
}
|
|
8635
|
+
|
|
8636
|
+
/// What `craft.navigation.back()` does.
|
|
8637
|
+
@objc private func backButtonTapped() {
|
|
8638
|
+
if !isAppScript, navigationController?.topViewController === self {
|
|
8639
|
+
navigationController?.popViewController(animated: true)
|
|
8640
|
+
}
|
|
8641
|
+
}
|
|
8642
|
+
|
|
8578
8643
|
private func navigationButton(_ spec: [String: Any]) -> UIBarButtonItem? {
|
|
8579
8644
|
guard let id = spec["id"] as? String, !id.isEmpty else { return nil }
|
|
8580
8645
|
let action = UIAction { [weak self] _ in
|
|
@@ -9060,27 +9125,44 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
9060
9125
|
/// view is does not come into it: a page of a pager that is off to one
|
|
9061
9126
|
/// side now is under the actions once it is swiped to. The safe area and
|
|
9062
9127
|
/// the viewport are left alone: content still scrolls behind the
|
|
9063
|
-
/// actions.
|
|
9128
|
+
/// actions.
|
|
9129
|
+
///
|
|
9130
|
+
/// Two things are not behind the actions, and get none. A scroll view
|
|
9131
|
+
/// that is not inside the shell the actions float in: a sheet is
|
|
9132
|
+
/// presented in front of them, and its scroll view was given the room of
|
|
9133
|
+
/// a capsule it covers, so every sheet could be dragged up into blank
|
|
9134
|
+
/// space. And a scroll view or list inside another that scrolls the same
|
|
9135
|
+
/// way: it is brought clear by the outer one, and where it sits in a page
|
|
9136
|
+
/// not yet scrolled to it says nothing about the glass; a short list
|
|
9137
|
+
/// below the fold was given the height of the page above it. Internal
|
|
9138
|
+
/// for XCTest.
|
|
9064
9139
|
func insetScrollViewsForFloatingActions() {
|
|
9065
9140
|
let chrome = CraftChrome.shared
|
|
9066
|
-
let
|
|
9067
|
-
|
|
9068
|
-
}
|
|
9141
|
+
let host = chrome.floatingActionsHost.flatMap { view.isDescendant(of: $0) ? $0 : nil }
|
|
9142
|
+
let actions = host == nil ? nil : chrome.floatingActions
|
|
9069
9143
|
func cover(_ scroll: UIScrollView) -> CGFloat {
|
|
9070
|
-
guard let actions, let window = scroll.window else { return 0 }
|
|
9144
|
+
guard let actions, let host, let window = scroll.window, scroll.isDescendant(of: host) else { return 0 }
|
|
9071
9145
|
let frame = scroll.convert(scroll.bounds, to: window)
|
|
9146
|
+
// Nothing past the end of the shell is on the glass.
|
|
9147
|
+
let end = min(frame.maxY, host.convert(host.bounds, to: window).maxY)
|
|
9072
9148
|
// An edge-to-edge scroller already clears the tab bar through
|
|
9073
9149
|
// UIKit's safe area. Add only what the actions reach above it.
|
|
9074
9150
|
let automaticBottom = max(0, scroll.adjustedContentInset.bottom - scroll.contentInset.bottom)
|
|
9075
|
-
return max(0,
|
|
9151
|
+
return max(0, end - actions.minY - automaticBottom)
|
|
9076
9152
|
}
|
|
9077
|
-
func visit(_ node: RenderedNode
|
|
9153
|
+
func visit(_ node: RenderedNode?, inside: Bool) {
|
|
9078
9154
|
guard let node else { return }
|
|
9079
|
-
|
|
9080
|
-
|
|
9081
|
-
|
|
9155
|
+
var inside = inside
|
|
9156
|
+
if let scroll = node.view as? CraftNativeScrollView, scroll.isVertical {
|
|
9157
|
+
scroll.floatingCover = inside ? 0 : cover(scroll)
|
|
9158
|
+
inside = true
|
|
9159
|
+
} else if let list = node.view as? CraftNativeFlatList {
|
|
9160
|
+
list.floatingCover = inside ? 0 : cover(list)
|
|
9161
|
+
if (list.collectionViewLayout as? UICollectionViewFlowLayout)?.scrollDirection != .horizontal { inside = true }
|
|
9162
|
+
}
|
|
9163
|
+
node.children.forEach { visit($0, inside: inside) }
|
|
9082
9164
|
}
|
|
9083
|
-
visit(renderedRoot)
|
|
9165
|
+
visit(renderedRoot, inside: false)
|
|
9084
9166
|
floatingClearanceApplied = actions != nil
|
|
9085
9167
|
}
|
|
9086
9168
|
|
|
@@ -11550,10 +11632,14 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
11550
11632
|
if let spacing = styleNumber(style["letterSpacing"]) { attributes[.kern] = spacing }
|
|
11551
11633
|
addTextDecorationAttributes(style, to: &attributes)
|
|
11552
11634
|
if let shadow = textShadow(style) { attributes[.shadow] = shadow }
|
|
11635
|
+
// A CSS line box may be shorter than its font while glyphs paint outside
|
|
11636
|
+
// it. UIKit clips that same paragraph inside a UILabel. Preserve the
|
|
11637
|
+
// resolved font metrics for layout and drawing, including custom faces
|
|
11638
|
+
// whose ascenders exceed their point size and Dynamic Type scaling.
|
|
11553
11639
|
if let lineHeight = styleNumber(style["lineHeight"]) {
|
|
11554
11640
|
let paragraph = NSMutableParagraphStyle()
|
|
11555
|
-
paragraph.minimumLineHeight = lineHeight
|
|
11556
|
-
paragraph.maximumLineHeight = lineHeight
|
|
11641
|
+
paragraph.minimumLineHeight = max(lineHeight, label.font.lineHeight)
|
|
11642
|
+
paragraph.maximumLineHeight = max(lineHeight, label.font.lineHeight)
|
|
11557
11643
|
paragraph.alignment = label.textAlignment
|
|
11558
11644
|
attributes[.paragraphStyle] = paragraph
|
|
11559
11645
|
}
|
|
@@ -11607,8 +11693,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
11607
11693
|
if let spacing = styleNumber(style["letterSpacing"]) { attributes[.kern] = spacing }
|
|
11608
11694
|
if let lineHeight = styleNumber(style["lineHeight"]) {
|
|
11609
11695
|
let paragraph = NSMutableParagraphStyle()
|
|
11610
|
-
paragraph.minimumLineHeight = lineHeight
|
|
11611
|
-
paragraph.maximumLineHeight = lineHeight
|
|
11696
|
+
paragraph.minimumLineHeight = max(lineHeight, font.lineHeight)
|
|
11697
|
+
paragraph.maximumLineHeight = max(lineHeight, font.lineHeight)
|
|
11612
11698
|
attributes[.paragraphStyle] = paragraph
|
|
11613
11699
|
}
|
|
11614
11700
|
addTextDecorationAttributes(style, to: &attributes)
|
|
@@ -11635,8 +11721,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
11635
11721
|
// then paints a second line outside the field instead of scrolling.
|
|
11636
11722
|
if singleLine { paragraph.lineBreakMode = .byClipping }
|
|
11637
11723
|
if let lineHeight = styleNumber(style["lineHeight"]) {
|
|
11638
|
-
paragraph.minimumLineHeight = lineHeight
|
|
11639
|
-
paragraph.maximumLineHeight = lineHeight
|
|
11724
|
+
paragraph.minimumLineHeight = max(lineHeight, font.lineHeight)
|
|
11725
|
+
paragraph.maximumLineHeight = max(lineHeight, font.lineHeight)
|
|
11640
11726
|
}
|
|
11641
11727
|
attributes[.paragraphStyle] = paragraph
|
|
11642
11728
|
addTextDecorationAttributes(style, to: &attributes)
|