@craft-native/ios 0.0.502 → 0.0.504

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@craft-native/ios",
3
- "version": "0.0.502",
3
+ "version": "0.0.504",
4
4
  "homepage": "https://github.com/craft-native/craft/tree/main/packages/ios#readme",
5
5
  "repository": {
6
6
  "type": "git",
@@ -4232,6 +4232,14 @@ private class CraftNativeFlowView: UIStackView {
4232
4232
  private final class CraftNativeSafeAreaView: CraftNativeFlowView {
4233
4233
  private var edges: Set<String> = ["top", "right", "bottom", "left"]
4234
4234
 
4235
+ /// The screen's leading scroll view owns the status bar's inset, and this
4236
+ /// view sits empty above it (trackContentScrollView). It takes no top
4237
+ /// inset then: taking it would make it as tall as the status bar, and the
4238
+ /// scroll view would no longer be the first thing on the screen.
4239
+ var yieldsTopInset = false {
4240
+ didSet { if yieldsTopInset != oldValue { updateSafeAreaPadding() } }
4241
+ }
4242
+
4235
4243
  func setSafeAreaEdges(_ raw: Any?) {
4236
4244
  guard let values = raw as? [Any] else {
4237
4245
  edges = ["top", "right", "bottom", "left"]
@@ -4253,7 +4261,7 @@ private final class CraftNativeSafeAreaView: CraftNativeFlowView {
4253
4261
  private func updateSafeAreaPadding() {
4254
4262
  let insets = safeAreaInsets
4255
4263
  safeAreaPadding = UIEdgeInsets(
4256
- top: edges.contains("top") ? insets.top : 0,
4264
+ top: edges.contains("top") && !yieldsTopInset ? insets.top : 0,
4257
4265
  left: edges.contains("left") ? insets.left : 0,
4258
4266
  bottom: edges.contains("bottom") ? insets.bottom : 0,
4259
4267
  right: edges.contains("right") ? insets.right : 0
@@ -8425,6 +8433,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
8425
8433
  return titleFromOptions || prefersLargeTitle == true || !(navigationItem.rightBarButtonItems ?? []).isEmpty
8426
8434
  }
8427
8435
  private weak var trackedScrollView: UIScrollView?
8436
+ /// Empty SafeAreaViews above the leading scroll view, which owns their top inset.
8437
+ private let yieldedSafeAreas = NSHashTable<CraftNativeSafeAreaView>.weakObjects()
8428
8438
 
8429
8439
  /// `craft.navigation.setOptions`: this screen's bar. Every key is
8430
8440
  /// optional and only the keys given change.
@@ -8839,17 +8849,39 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
8839
8849
  }
8840
8850
  /// The scroll view the screen starts with, when nothing sits above it:
8841
8851
  /// the root's first child, its first, and so on, none padded on top.
8852
+ /// Empty siblings above it do not count. An empty SafeAreaView among
8853
+ /// them is remembered: once the root runs under the status bar it
8854
+ /// would take that inset and stop being empty, the scroll view would
8855
+ /// stop leading, the root would return inside the safe area and the
8856
+ /// view would be empty again. A screen with an empty header above its
8857
+ /// scroll view flipped between the two layouts and stopped in either.
8858
+ var emptySafeAreasAbove: [CraftNativeSafeAreaView] = []
8842
8859
  func leading(_ node: RenderedNode) -> RenderedNode? {
8843
8860
  if let scroll = node.view as? CraftNativeScrollView { return scroll.isVertical && scroll.isScrollEnabled ? node : nil }
8844
8861
  if let list = node.view as? CraftNativeFlatList {
8845
8862
  return list.isScrollEnabled && (list.collectionViewLayout as? UICollectionViewFlowLayout)?.scrollDirection != .horizontal ? node : nil
8846
8863
  }
8847
8864
  guard paddingInsets(node.style).top == 0,
8848
- let first = node.children.first(where: { $0.type != "Modal" && !$0.view.isHidden && $0.view.bounds.height > 0 }) else { return nil }
8849
- return leading(first)
8865
+ let index = node.children.firstIndex(where: { $0.type != "Modal" && !$0.view.isHidden && $0.view.bounds.height > 0 }) else { return nil }
8866
+ for skipped in node.children[..<index] where skipped.type != "Modal" && !skipped.view.isHidden {
8867
+ if let safeArea = skipped.view as? CraftNativeSafeAreaView { emptySafeAreasAbove.append(safeArea) }
8868
+ }
8869
+ return leading(node.children[index])
8850
8870
  }
8851
8871
  let scroll = find(renderedRoot)
8852
8872
  let leadingScroll = leading(renderedRoot)
8873
+ // The leading scroll view owns the top inset; the empty SafeAreaViews
8874
+ // it passed give theirs up, and get it back when it no longer leads
8875
+ // (the header gained content, or the scroll view left the screen).
8876
+ let yielding = leadingScroll == nil ? [] : emptySafeAreasAbove
8877
+ for view in yieldedSafeAreas.allObjects where !yielding.contains(where: { $0 === view }) {
8878
+ view.yieldsTopInset = false
8879
+ yieldedSafeAreas.remove(view)
8880
+ }
8881
+ for view in yielding where !view.yieldsTopInset {
8882
+ view.yieldsTopInset = true
8883
+ yieldedSafeAreas.add(view)
8884
+ }
8853
8885
  // The leading scroller may overflow beneath navigation chrome.
8854
8886
  // A bounded list inside a form must clip its rows to its viewport.
8855
8887
  if let leadingScroll, leadingScroll.style["overflow"] as? String != "hidden" {
@@ -11632,10 +11664,14 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
11632
11664
  if let spacing = styleNumber(style["letterSpacing"]) { attributes[.kern] = spacing }
11633
11665
  addTextDecorationAttributes(style, to: &attributes)
11634
11666
  if let shadow = textShadow(style) { attributes[.shadow] = shadow }
11667
+ // A CSS line box may be shorter than its font while glyphs paint outside
11668
+ // it. UIKit clips that same paragraph inside a UILabel. Preserve the
11669
+ // resolved font metrics for layout and drawing, including custom faces
11670
+ // whose ascenders exceed their point size and Dynamic Type scaling.
11635
11671
  if let lineHeight = styleNumber(style["lineHeight"]) {
11636
11672
  let paragraph = NSMutableParagraphStyle()
11637
- paragraph.minimumLineHeight = lineHeight
11638
- paragraph.maximumLineHeight = lineHeight
11673
+ paragraph.minimumLineHeight = max(lineHeight, label.font.lineHeight)
11674
+ paragraph.maximumLineHeight = max(lineHeight, label.font.lineHeight)
11639
11675
  paragraph.alignment = label.textAlignment
11640
11676
  attributes[.paragraphStyle] = paragraph
11641
11677
  }
@@ -11689,8 +11725,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
11689
11725
  if let spacing = styleNumber(style["letterSpacing"]) { attributes[.kern] = spacing }
11690
11726
  if let lineHeight = styleNumber(style["lineHeight"]) {
11691
11727
  let paragraph = NSMutableParagraphStyle()
11692
- paragraph.minimumLineHeight = lineHeight
11693
- paragraph.maximumLineHeight = lineHeight
11728
+ paragraph.minimumLineHeight = max(lineHeight, font.lineHeight)
11729
+ paragraph.maximumLineHeight = max(lineHeight, font.lineHeight)
11694
11730
  attributes[.paragraphStyle] = paragraph
11695
11731
  }
11696
11732
  addTextDecorationAttributes(style, to: &attributes)
@@ -11717,8 +11753,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
11717
11753
  // then paints a second line outside the field instead of scrolling.
11718
11754
  if singleLine { paragraph.lineBreakMode = .byClipping }
11719
11755
  if let lineHeight = styleNumber(style["lineHeight"]) {
11720
- paragraph.minimumLineHeight = lineHeight
11721
- paragraph.maximumLineHeight = lineHeight
11756
+ paragraph.minimumLineHeight = max(lineHeight, font.lineHeight)
11757
+ paragraph.maximumLineHeight = max(lineHeight, font.lineHeight)
11722
11758
  }
11723
11759
  attributes[.paragraphStyle] = paragraph
11724
11760
  addTextDecorationAttributes(style, to: &attributes)