@momo-kits/native-kits 0.162.47 → 0.162.48
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/CLAUDE.md +1 -1
- package/ios/Layout/ScreenLayout.swift +41 -11
- package/ios/Popup/PopupLayout.swift +0 -2
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -71,7 +71,7 @@ Publishing is automated (`publish.sh` + GitLab CI on `[ci build]` / `main`). Do
|
|
|
71
71
|
## Screen layout / iPhone Duo (port of RN `useScreenLayout`, momo-kits !664)
|
|
72
72
|
|
|
73
73
|
- `layout/ScreenLayout.kt`: `getScreenLayout()` returns window size, raw top/bottom + side insets, `contentWidth`, header geometry, the iPhone Duo `sideBar` and `duoTopInset`. `NavigationContainer` computes it once and provides `LocalScreenLayout`; reactive to fold/unfold and rotation (window size + `WindowInsets`). Metrics (`DuoMetrics`, 72pt iPhone header floor, 20pt status-bar threshold) mirror RN `Layout/utils.ts` — change both together.
|
|
74
|
-
- iPhone-only: everything gates on `isPhoneIdiom()` (`platform/`), never on "iOS", so iPad windows keep the regular layout. Two detectors, as in RN: `isIPhoneDuo` (window aspect < 1.6 or asymmetric side insets) drives layout; `isIPhoneDuoDevice` (
|
|
74
|
+
- iPhone-only: everything gates on `isPhoneIdiom()` (`platform/`), never on "iOS", so iPad windows keep the regular layout. Two detectors, as in RN: `isIPhoneDuo` (window aspect < 1.6 or asymmetric side insets) drives layout; `isIPhoneDuoDevice` (Apple's foldable check: a `.division` reserved region, queried with include-inactive; Swift also latches on `UIHingeInteraction`) drives popups, modal and bottom sheet.
|
|
75
75
|
- Header height is `headerHeight` (not `AppStatusBar + HEADER_HEIGHT`); the bar row is `headerBarHeight` and starts `headerStatusBarHeight` below the window top. When the status bar is off the top edge (iPhone landscape, iPhone Duo) `headerStatusBarHeight` is 0 so the title stays centered — except on the Duo outer display: in portrait the header is 76pt and the row starts at 14pt so the title centers on the camera punchout; in landscape the row centers on the camera when it sits at the top end of the strip (else on the back button) and the header reaches 8pt below the back button.
|
|
76
76
|
- iPhone Duo inner display in portrait (status bar on top): the header row shares the status bar band — `headerHeight` 94pt (2 × the clock digits' y 47), `headerStatusBarHeight` 0 — and `ScreenLayout.statusBarClearance` keeps it between the clock / battery clusters (from reserved regions, else 128pt on the right). Back and toolkit use the 48pt side bar controls laid out horizontally (`ToolkitLayout.SideBarRow`); the collapsed search and a centered title stay clear of the clusters. Port of momo-kits `90af33ba2`.
|
|
77
77
|
- iPhone Duo with a side strip ≥ 60pt: back + toolkit leave the bar for `HeaderSideBar` (`navigation/component/SideBar.kt`, 48pt glass controls); custom header-right stays in the bar. `sideBar.top` is 170pt in portrait, 120pt on the inner display in landscape (height ≥ 600), 24pt on the outer display in landscape, or below the header when the status bar is on top; UIKit's `statusBarFrame` is useless on Duo. Unlike RN the column is clamped above footer (24pt gap) / rail (12pt gap).
|
|
@@ -32,6 +32,8 @@ public struct ScreenLayout: Equatable {
|
|
|
32
32
|
public let width: CGFloat
|
|
33
33
|
public let height: CGFloat
|
|
34
34
|
public let isIPhoneDuo: Bool
|
|
35
|
+
/// The device has a hinge, folded or not, whatever the window size; drives popups, modal and bottom sheet.
|
|
36
|
+
public let isDuoDevice: Bool
|
|
35
37
|
public let insets: EdgeInsets
|
|
36
38
|
public let insetLeft: CGFloat
|
|
37
39
|
public let insetRight: CGFloat
|
|
@@ -73,8 +75,6 @@ enum DuoMetrics {
|
|
|
73
75
|
static let floatingTabBarHeight: CGFloat = 62
|
|
74
76
|
static let floatingTabBarMargin: CGFloat = 20
|
|
75
77
|
static let floatingTabBarInsetOverlap: CGFloat = 13
|
|
76
|
-
// Widest short side of any single-display iPhone.
|
|
77
|
-
static let iPhoneMaxShortSide: CGFloat = 440
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
enum HeaderMetrics {
|
|
@@ -109,15 +109,17 @@ extension ScreenLayout {
|
|
|
109
109
|
size: CGSize,
|
|
110
110
|
insets: EdgeInsets,
|
|
111
111
|
reservedRegions: [CGRect] = ReservedRegions.current,
|
|
112
|
-
isPhone: Bool = isPhoneIdiom
|
|
112
|
+
isPhone: Bool = isPhoneIdiom,
|
|
113
|
+
isFoldable: Bool = FoldableDevice.isFoldable(measuredIn: keyWindow)
|
|
113
114
|
) -> ScreenLayout {
|
|
114
115
|
let width = size.width
|
|
115
116
|
let height = size.height
|
|
117
|
+
let isDuoDevice = isPhone && isFoldable
|
|
116
118
|
var (headerHeight, headerStatusBarHeight) = headerGeometry(topInset: insets.top, isPhone: isPhone)
|
|
117
119
|
let isDuo = isPhone && isIPhoneDuoWindow(width: width, height: height, insets: insets)
|
|
118
120
|
guard isDuo else {
|
|
119
121
|
return ScreenLayout(
|
|
120
|
-
width: width, height: height, isIPhoneDuo: false, insets: insets,
|
|
122
|
+
width: width, height: height, isIPhoneDuo: false, isDuoDevice: isDuoDevice, insets: insets,
|
|
121
123
|
insetLeft: 0, insetRight: 0, contentWidth: width,
|
|
122
124
|
headerHeight: headerHeight, headerStatusBarHeight: headerStatusBarHeight, sideBar: nil,
|
|
123
125
|
statusBarClearance: nil
|
|
@@ -177,7 +179,7 @@ extension ScreenLayout {
|
|
|
177
179
|
}
|
|
178
180
|
|
|
179
181
|
return ScreenLayout(
|
|
180
|
-
width: width, height: height, isIPhoneDuo: true, insets: insets,
|
|
182
|
+
width: width, height: height, isIPhoneDuo: true, isDuoDevice: isDuoDevice, insets: insets,
|
|
181
183
|
insetLeft: insets.leading, insetRight: insets.trailing,
|
|
182
184
|
contentWidth: width - insets.leading - insets.trailing,
|
|
183
185
|
headerHeight: headerHeight, headerStatusBarHeight: headerStatusBarHeight, sideBar: sideBar,
|
|
@@ -256,13 +258,40 @@ extension ScreenLayout {
|
|
|
256
258
|
}
|
|
257
259
|
}
|
|
258
260
|
|
|
259
|
-
///
|
|
260
|
-
///
|
|
261
|
+
/// Snapshot for call sites without `\.screenLayout`; not reactive. `windowSize` is ignored,
|
|
262
|
+
/// kept for host call sites.
|
|
261
263
|
public func isIPhoneDuoDevice(windowSize: CGSize? = nil) -> Bool {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
264
|
+
isPhoneIdiom && FoldableDevice.isFoldable(measuredIn: keyWindow)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/// Apple's foldable check (as the host's `SideTabRailView.isFoldableDevice`): only a device with a
|
|
268
|
+
/// hinge reports a `.division` region, inactive while flat but still there. Screen size cannot tell
|
|
269
|
+
/// it apart from a large iPhone in landscape or a narrowed window.
|
|
270
|
+
enum FoldableDevice {
|
|
271
|
+
// a hinge never goes away, so a positive answer is kept
|
|
272
|
+
private static var isKnownFoldable = false
|
|
273
|
+
|
|
274
|
+
static func isFoldable(measuredIn view: UIView?) -> Bool {
|
|
275
|
+
if isKnownFoldable { return true }
|
|
276
|
+
// compiled out on SDKs older than iOS 27.1 (UIKit 9127.0.85.28)
|
|
277
|
+
#if canImport(UIKit, _version: 9127.0.85.28)
|
|
278
|
+
guard #available(iOS 27.1, *), let view, view.window != nil || view is UIWindow else { return false }
|
|
279
|
+
isKnownFoldable = !view.reservedRegions(kind: .division, options: .includeInactive).isEmpty
|
|
280
|
+
#endif
|
|
281
|
+
return isKnownFoldable
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/// Catches the hinge when the regions were read too early (before the window had them).
|
|
285
|
+
static func observe(in view: UIView) {
|
|
286
|
+
#if canImport(UIKit, _version: 9127.0.85.28)
|
|
287
|
+
guard #available(iOS 27.1, *) else { return }
|
|
288
|
+
view.addInteraction(UIHingeInteraction { _, update in
|
|
289
|
+
guard update.hinge != nil, !isKnownFoldable else { return }
|
|
290
|
+
isKnownFoldable = true
|
|
291
|
+
NotificationCenter.default.post(name: ReservedRegions.didChange, object: nil)
|
|
292
|
+
})
|
|
293
|
+
#endif
|
|
294
|
+
}
|
|
266
295
|
}
|
|
267
296
|
|
|
268
297
|
// MARK: - Reserved regions
|
|
@@ -287,6 +316,7 @@ private final class ReservedRegionsProbe: UIView {
|
|
|
287
316
|
override init(frame: CGRect) {
|
|
288
317
|
super.init(frame: frame)
|
|
289
318
|
isUserInteractionEnabled = false
|
|
319
|
+
FoldableDevice.observe(in: self)
|
|
290
320
|
}
|
|
291
321
|
|
|
292
322
|
required init?(coder: NSCoder) { nil }
|
|
@@ -5,8 +5,6 @@ import SwiftUI
|
|
|
5
5
|
private let duoPopupMaxWidth: CGFloat = 440
|
|
6
6
|
|
|
7
7
|
extension ScreenLayout {
|
|
8
|
-
var isDuoDevice: Bool { isIPhoneDuoDevice(windowSize: CGSize(width: width, height: height)) }
|
|
9
|
-
|
|
10
8
|
/// Card width cap (Compose `popupMaxWidth`); unbounded off iPhone Duo, where the popup's own padding sets the width.
|
|
11
9
|
var popupMaxWidth: CGFloat {
|
|
12
10
|
isDuoDevice ? duoPopupMaxWidth : .infinity
|