craft-native 0.0.497 → 0.0.500
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/dist/android/templates/CraftAppLinks.kt.template +28 -0
- package/dist/android/templates/CraftBridge.kt.template +3 -29
- package/dist/android/templates/CraftNativeBackground.kt.template +21 -2
- package/dist/android/templates/CraftNativeCapabilities.kt.template +145 -28
- package/dist/android/templates/CraftNativeFlatList.kt.template +51 -3
- package/dist/android/templates/CraftNativeShell.kt.template +102 -2
- package/dist/android/templates/CraftNativeSounds.kt.template +22 -3
- package/dist/android/templates/MainActivityNative.kt.template +44 -14
- package/dist/api/process.d.ts +23 -28
- package/dist/bridge/core.d.ts +30 -6
- package/dist/browser.js +27 -5
- package/dist/cli.js +39 -19
- package/dist/components/index.d.ts +1 -1
- package/dist/index.cjs +90 -99
- package/dist/index.js +90 -99
- package/dist/ios/templates/CraftApp.swift +25 -1
- package/dist/ios/templates/CraftHybrid.swift +63 -22
- package/dist/ios/templates/CraftNativeActions.swift +47 -3
- package/dist/ios/templates/CraftNativeFiles.swift +160 -8
- package/dist/ios/templates/CraftNativeFlatList.swift +43 -6
- package/dist/ios/templates/CraftNativeScreen.swift +222 -69
- package/dist/ios/templates/CraftPersistentStorage.swift +28 -5
- package/dist/ios/templates/CraftScheduledWorkouts.swift +11 -4
- package/dist/ios/templates/CraftWebAudio.swift +20 -1
- package/dist/package.d.ts +22 -0
- package/package.json +1 -1
|
@@ -150,6 +150,38 @@ func craftNativeNumber(_ value: Any?) -> NSNumber? {
|
|
|
150
150
|
return number
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
/// A number from a prop as an `Int`, the way `toInt()` gives it on the
|
|
154
|
+
/// Android host: toward zero, the nearest end of the 32-bit range when it is
|
|
155
|
+
/// outside it, and 0 when it is not a number.
|
|
156
|
+
///
|
|
157
|
+
/// `Int(_:)` traps on a value it cannot hold, and a prop is whatever the
|
|
158
|
+
/// screen's script said: `maxLength={Number.MAX_VALUE}` ended the process
|
|
159
|
+
/// (#510). The 32-bit range is the other host's, and it leaves room for the
|
|
160
|
+
/// arithmetic done on the result, which `Int.max` would not.
|
|
161
|
+
func craftNativeInt(_ value: Double) -> Int {
|
|
162
|
+
guard !value.isNaN else { return 0 }
|
|
163
|
+
return Int(max(Double(Int32.min), min(Double(Int32.max), value.rounded(.towardZero))))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
func craftNativeInt(_ value: CGFloat) -> Int { craftNativeInt(Double(value)) }
|
|
167
|
+
|
|
168
|
+
/// The most a number may be in layout, either side of zero: 2^25 points. It
|
|
169
|
+
/// is far past any screen, and small enough that what the layout makes of a
|
|
170
|
+
/// few such numbers multiplied together still fits a Float, which the flex
|
|
171
|
+
/// engine works in.
|
|
172
|
+
let craftNativeLayoutLimit: CGFloat = 33_554_432
|
|
173
|
+
|
|
174
|
+
/// A number for layout: inside that limit, and 0 when it is not a number.
|
|
175
|
+
///
|
|
176
|
+
/// A style is whatever the script said. `flex: Number.MAX_VALUE` is finite,
|
|
177
|
+
/// and became an infinite share of a row, an infinite frame, and then
|
|
178
|
+
/// UIKit's `NSLayoutConstraint constant is not finite! That's illegal`, an
|
|
179
|
+
/// exception nothing catches: the app ended. Lengths and flex factors are
|
|
180
|
+
/// held to this as they are read, and so is every frame the layout produces.
|
|
181
|
+
func craftNativeLayoutNumber(_ value: CGFloat) -> CGFloat {
|
|
182
|
+
value.isNaN ? 0 : max(-craftNativeLayoutLimit, min(craftNativeLayoutLimit, value))
|
|
183
|
+
}
|
|
184
|
+
|
|
153
185
|
/// A finite number from a prop, for the views outside the screen controller,
|
|
154
186
|
/// which has its own: the keyboard-avoiding view and SVG text read theirs
|
|
155
187
|
/// with it, and without it the template did not compile.
|
|
@@ -161,11 +193,19 @@ private func number(_ value: Any?) -> CGFloat? {
|
|
|
161
193
|
return value.isFinite ? value : nil
|
|
162
194
|
}
|
|
163
195
|
|
|
196
|
+
/// A finite number from a style, held to what layout can use (see
|
|
197
|
+
/// `craftNativeLayoutNumber`). A padding, radius, spacing or size of
|
|
198
|
+
/// `Number.MAX_VALUE` reached UIKit as an inset or a font it could not place,
|
|
199
|
+
/// and UIKit raised `CALayer position contains NaN`: the app ended.
|
|
200
|
+
private func styleNumber(_ value: Any?) -> CGFloat? {
|
|
201
|
+
number(value).map(craftNativeLayoutNumber)
|
|
202
|
+
}
|
|
203
|
+
|
|
164
204
|
private func craftNativeFontWeight(_ value: Any?) -> String? {
|
|
165
205
|
if let value = value as? String { return value }
|
|
166
206
|
guard let number = craftNativeNumber(value), number.doubleValue.isFinite else { return nil }
|
|
167
207
|
let integer = number.doubleValue.rounded()
|
|
168
|
-
return integer == number.doubleValue ? String(
|
|
208
|
+
return integer == number.doubleValue ? String(craftNativeInt(integer)) : nil
|
|
169
209
|
}
|
|
170
210
|
|
|
171
211
|
private func craftNativePickerValue(_ value: Any?) -> String? {
|
|
@@ -2389,16 +2429,16 @@ private enum CraftNativeLength: Equatable {
|
|
|
2389
2429
|
if let number = craftNativeNumber(value) {
|
|
2390
2430
|
let points = CGFloat(truncating: number)
|
|
2391
2431
|
guard points.isFinite else { return nil }
|
|
2392
|
-
self = .points(points)
|
|
2432
|
+
self = .points(craftNativeLayoutNumber(points))
|
|
2393
2433
|
return
|
|
2394
2434
|
}
|
|
2395
2435
|
guard let text = (value as? String)?.trimmingCharacters(in: .whitespaces), !text.isEmpty else { return nil }
|
|
2396
2436
|
if text.hasSuffix("%"), let fraction = Double(text.dropLast()), fraction.isFinite {
|
|
2397
|
-
self = .percent(CGFloat(fraction) / 100)
|
|
2437
|
+
self = .percent(craftNativeLayoutNumber(CGFloat(fraction) / 100))
|
|
2398
2438
|
} else if text.hasSuffix("px"), let points = Double(text.dropLast(2)), points.isFinite {
|
|
2399
|
-
self = .points(CGFloat(points))
|
|
2439
|
+
self = .points(craftNativeLayoutNumber(CGFloat(points)))
|
|
2400
2440
|
} else if let points = Double(text), points.isFinite {
|
|
2401
|
-
self = .points(CGFloat(points))
|
|
2441
|
+
self = .points(craftNativeLayoutNumber(CGFloat(points)))
|
|
2402
2442
|
} else {
|
|
2403
2443
|
return nil
|
|
2404
2444
|
}
|
|
@@ -2449,7 +2489,7 @@ private struct CraftNativeLayoutStyle {
|
|
|
2449
2489
|
func number(_ value: Any?) -> CGFloat? {
|
|
2450
2490
|
guard let number = craftNativeNumber(value) else { return nil }
|
|
2451
2491
|
let value = CGFloat(truncating: number)
|
|
2452
|
-
return value.isFinite ? value : nil
|
|
2492
|
+
return value.isFinite ? craftNativeLayoutNumber(value) : nil
|
|
2453
2493
|
}
|
|
2454
2494
|
width = CraftNativeLength(raw["width"])
|
|
2455
2495
|
height = CraftNativeLength(raw["height"])
|
|
@@ -2466,7 +2506,7 @@ private struct CraftNativeLayoutStyle {
|
|
|
2466
2506
|
left = number(raw["left"])
|
|
2467
2507
|
let align = raw["alignSelf"] as? String
|
|
2468
2508
|
alignSelf = align == "auto" ? nil : align
|
|
2469
|
-
order = number(raw["order"]).map {
|
|
2509
|
+
order = number(raw["order"]).map { craftNativeInt($0) } ?? 0
|
|
2470
2510
|
// `flex: n` is the web's and React Native's shorthand: a positive
|
|
2471
2511
|
// number grows by n, shrinks, and starts from nothing, so `flex-1`
|
|
2472
2512
|
// siblings share a row equally whatever their content.
|
|
@@ -2832,7 +2872,7 @@ enum CraftNativeFlexLayout {
|
|
|
2832
2872
|
static func ceiled(_ size: CGSize, scale: CGFloat, atLeast floor: CGSize) -> CGSize {
|
|
2833
2873
|
let scale = max(1, scale)
|
|
2834
2874
|
// Less a hair, so a size already on the pixel grid stays put.
|
|
2835
|
-
func up(_ value: CGFloat) -> CGFloat { (value * scale - 0.001).rounded(.up) / scale }
|
|
2875
|
+
func up(_ value: CGFloat) -> CGFloat { (craftNativeLayoutNumber(value) * scale - 0.001).rounded(.up) / scale }
|
|
2836
2876
|
return CGSize(width: max(floor.width, up(size.width)), height: max(floor.height, up(size.height)))
|
|
2837
2877
|
}
|
|
2838
2878
|
|
|
@@ -2840,7 +2880,9 @@ enum CraftNativeFlexLayout {
|
|
|
2840
2880
|
/// neither overlap nor leave a hairline between them.
|
|
2841
2881
|
static func snapped(_ rect: CGRect, scale: CGFloat) -> CGRect {
|
|
2842
2882
|
let scale = max(1, scale)
|
|
2843
|
-
|
|
2883
|
+
// Whatever the arithmetic before this made of the styles it was
|
|
2884
|
+
// given, the frame UIKit is handed is one it accepts.
|
|
2885
|
+
func snap(_ value: CGFloat) -> CGFloat { (craftNativeLayoutNumber(value) * scale).rounded() / scale }
|
|
2844
2886
|
let minX = snap(rect.minX), minY = snap(rect.minY)
|
|
2845
2887
|
return CGRect(x: minX, y: minY, width: max(0, snap(rect.maxX) - minX), height: max(0, snap(rect.maxY) - minY))
|
|
2846
2888
|
}
|
|
@@ -3314,6 +3356,7 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
3314
3356
|
if moved {
|
|
3315
3357
|
craftScreenController?.scheduleLayoutEvents()
|
|
3316
3358
|
craftScreenController?.scheduleKeyboardInsets()
|
|
3359
|
+
craftScreenController?.scheduleFloatingClearance()
|
|
3317
3360
|
}
|
|
3318
3361
|
}
|
|
3319
3362
|
for (child, frame) in result.frames {
|
|
@@ -4798,14 +4841,23 @@ final class CraftPressFeedbackDelegate: NSObject, UIGestureRecognizerDelegate {
|
|
|
4798
4841
|
|
|
4799
4842
|
/// Whether a scrolling view's gesture runs together with `other`. A scroll
|
|
4800
4843
|
/// view is the delegate of its own pan as well as of its keyboard tap, so this
|
|
4801
|
-
/// answers for both.
|
|
4844
|
+
/// answers for both. Most things run together (the keyboard tap watches
|
|
4802
4845
|
/// alongside a control's own gestures; the back-swipe and the tab swipe decide
|
|
4803
|
-
/// for themselves)
|
|
4804
|
-
///
|
|
4846
|
+
/// for themselves), with two exceptions for a scroll view's pan.
|
|
4847
|
+
///
|
|
4848
|
+
/// Another scroll view's pan: run together, a drag on a nested scroll view
|
|
4849
|
+
/// scrolled its parent at the same time.
|
|
4850
|
+
///
|
|
4851
|
+
/// A press. A row or a card that can be pressed is a view with a tap
|
|
4852
|
+
/// recognizer, and UIKit fails a tap the moment a drag begins on the scroll
|
|
4853
|
+
/// view around it. That is what keeps a flick, or the touch that stops a
|
|
4854
|
+
/// moving list, from also pressing whatever is under the finger. Told to run
|
|
4855
|
+
/// together, both happened: a short scroll pressed the thing it began on.
|
|
4805
4856
|
func craftScrollGesture(_ gesture: UIGestureRecognizer, runsWith other: UIGestureRecognizer) -> Bool {
|
|
4806
|
-
guard let scroll = gesture.view as? UIScrollView, gesture === scroll.panGestureRecognizer
|
|
4807
|
-
|
|
4808
|
-
return false
|
|
4857
|
+
guard let scroll = gesture.view as? UIScrollView, gesture === scroll.panGestureRecognizer else { return true }
|
|
4858
|
+
if let otherScroll = other.view as? UIScrollView, other === otherScroll.panGestureRecognizer { return false }
|
|
4859
|
+
if other is UITapGestureRecognizer, other.delegate is CraftPressGestureDelegate { return false }
|
|
4860
|
+
return true
|
|
4809
4861
|
}
|
|
4810
4862
|
|
|
4811
4863
|
/// Scroll containers observe taps without cancelling native input focus or
|
|
@@ -4992,7 +5044,7 @@ private final class CraftNativeScrollView: UIScrollView, UIGestureRecognizerDele
|
|
|
4992
5044
|
/// these indices stay at the top of the visible area once scrolled to,
|
|
4993
5045
|
/// each pushed up by the next. Vertical scroll views only.
|
|
4994
5046
|
func setStickyHeaderIndices(_ value: Any?) {
|
|
4995
|
-
let next = Array(Set(((value as? [Any]) ?? []).compactMap { number($0).map {
|
|
5047
|
+
let next = Array(Set(((value as? [Any]) ?? []).compactMap { number($0).map { craftNativeInt($0) } }.filter { $0 >= 0 })).sorted()
|
|
4996
5048
|
guard next != stickyIndices else { return }
|
|
4997
5049
|
stickyIndices = next
|
|
4998
5050
|
setNeedsLayout()
|
|
@@ -5108,6 +5160,19 @@ private final class CraftNativeScrollView: UIScrollView, UIGestureRecognizerDele
|
|
|
5108
5160
|
setContentNeedsLayout()
|
|
5109
5161
|
}
|
|
5110
5162
|
|
|
5163
|
+
/// What covers the end of this scroll view from below: the keyboard, and
|
|
5164
|
+
/// the compact tab actions floating above the tab bar (#523). The content
|
|
5165
|
+
/// is inset by the larger, so its last lines can be scrolled clear of
|
|
5166
|
+
/// whichever reaches further.
|
|
5167
|
+
var keyboardCover: CGFloat = 0 { didSet { applyCover() } }
|
|
5168
|
+
var floatingCover: CGFloat = 0 { didSet { applyCover() } }
|
|
5169
|
+
|
|
5170
|
+
private func applyCover() {
|
|
5171
|
+
let cover = max(keyboardCover, floatingCover)
|
|
5172
|
+
if abs(contentInset.bottom - cover) > 0.5 { contentInset.bottom = cover }
|
|
5173
|
+
if abs(verticalScrollIndicatorInsets.bottom - cover) > 0.5 { verticalScrollIndicatorInsets.bottom = cover }
|
|
5174
|
+
}
|
|
5175
|
+
|
|
5111
5176
|
private var refreshingProp = false
|
|
5112
5177
|
|
|
5113
5178
|
/// `onRefresh` and `refreshing`: the system's pull to refresh, controlled
|
|
@@ -5516,7 +5581,7 @@ final class CraftNativeSvgView: UIView {
|
|
|
5516
5581
|
var out = from
|
|
5517
5582
|
var i = 0
|
|
5518
5583
|
while i < from.count {
|
|
5519
|
-
let command =
|
|
5584
|
+
let command = craftNativeInt(from[i])
|
|
5520
5585
|
guard to[i] == from[i] else { return from }
|
|
5521
5586
|
let count: Int
|
|
5522
5587
|
switch command {
|
|
@@ -5701,7 +5766,7 @@ final class CraftNativeSvgView: UIView {
|
|
|
5701
5766
|
guard !text.isEmpty else { return }
|
|
5702
5767
|
let props = node["props"] as? [String: Any] ?? [:]
|
|
5703
5768
|
let style = node["style"] as? [String: Any] ?? [:]
|
|
5704
|
-
let size = number(props["fontSize"]) ??
|
|
5769
|
+
let size = number(props["fontSize"]) ?? styleNumber(style["fontSize"]) ?? 12
|
|
5705
5770
|
let weight: UIFont.Weight = {
|
|
5706
5771
|
switch paint.fontWeight?.lowercased() {
|
|
5707
5772
|
case "500": return .medium
|
|
@@ -5743,7 +5808,7 @@ final class CraftNativeSvgView: UIView {
|
|
|
5743
5808
|
var i = 0
|
|
5744
5809
|
func point(_ at: Int) -> CGPoint { CGPoint(x: n[at], y: n[at + 1]) }
|
|
5745
5810
|
while i < n.count {
|
|
5746
|
-
switch
|
|
5811
|
+
switch craftNativeInt(n[i]) {
|
|
5747
5812
|
case 0 where i + 2 < n.count: path.move(to: point(i + 1)); i += 3
|
|
5748
5813
|
case 1 where i + 2 < n.count: path.addLine(to: point(i + 1)); i += 3
|
|
5749
5814
|
case 2 where i + 6 < n.count: path.addCurve(to: point(i + 5), controlPoint1: point(i + 1), controlPoint2: point(i + 3)); i += 7
|
|
@@ -6848,6 +6913,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6848
6913
|
/// targeted update of this subtree inherits it without its ancestors.
|
|
6849
6914
|
var inheritedFontFamily: String?
|
|
6850
6915
|
var children: [RenderedNode] = []
|
|
6916
|
+
/// `autoFocus` was on when this input was last drawn. Focus is taken
|
|
6917
|
+
/// when it turns on, which mounting with it on is, and not on each
|
|
6918
|
+
/// render while it stays on (#511).
|
|
6919
|
+
var autoFocus = false
|
|
6851
6920
|
var widthConstraint: NSLayoutConstraint?
|
|
6852
6921
|
var heightConstraint: NSLayoutConstraint?
|
|
6853
6922
|
|
|
@@ -6930,6 +6999,17 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6930
6999
|
private var runningAnimation: CraftNativeRunningAnimation?
|
|
6931
7000
|
private var renderedRoot: RenderedNode?
|
|
6932
7001
|
private weak var lastFocusedInput: UIView?
|
|
7002
|
+
/// On its way off screen, or off it: from `viewWillDisappear` until the
|
|
7003
|
+
/// screen is next about to appear.
|
|
7004
|
+
///
|
|
7005
|
+
/// A screen ends editing as it starts to leave (#497), and its view stays
|
|
7006
|
+
/// in the window for the length of the transition. Whatever put focus
|
|
7007
|
+
/// back in that time brought the keyboard up for a screen that was then
|
|
7008
|
+
/// removed, where nothing could put it away: a render, and a sign-in
|
|
7009
|
+
/// screen makes one as it navigates; the acknowledgement an input had
|
|
7010
|
+
/// scheduled a tenth of a second before; a modal closing; `autoFocus`.
|
|
7011
|
+
/// Each of them asks this first.
|
|
7012
|
+
private var isLeavingScreen = false
|
|
6933
7013
|
|
|
6934
7014
|
/// The person put the keyboard away (Return, or a tap on nothing in a
|
|
6935
7015
|
/// scroll view): the last input is let go. Kept, the next render or
|
|
@@ -6995,6 +7075,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6995
7075
|
/// Where the keyboard is on the screen while it is up, in screen coordinates.
|
|
6996
7076
|
private var keyboardScreenFrame: CGRect?
|
|
6997
7077
|
private var keyboardInsetsScheduled = false
|
|
7078
|
+
private var floatingClearanceScheduled = false
|
|
7079
|
+
/// Whether any scroll view may still hold room for actions that have gone.
|
|
7080
|
+
private var floatingClearanceApplied = false
|
|
6998
7081
|
/// The hybrid shell (CraftHybrid.swift), when this screen is one of a web
|
|
6999
7082
|
/// app's native screens: told of the first frame, of a first frame that
|
|
7000
7083
|
/// could not be drawn, and of `craft.navigation.open(path)`.
|
|
@@ -7035,6 +7118,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7035
7118
|
let timers = self.timers
|
|
7036
7119
|
CraftNativeScriptThread.run { timers.pending.values.forEach { $0.cancel() } }
|
|
7037
7120
|
pendingCapabilityRequests.forEach { CraftNativeActions.cancel(requestToken: $0) }
|
|
7121
|
+
CraftNativeActions.endScope(capabilityScope)
|
|
7038
7122
|
lifecycleObservers.forEach(NotificationCenter.default.removeObserver)
|
|
7039
7123
|
if let deepLinkListener = deepLinkListener { DeepLinkManager.shared.removeNativeListener(deepLinkListener) }
|
|
7040
7124
|
}
|
|
@@ -7091,6 +7175,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7091
7175
|
|
|
7092
7176
|
override func viewWillAppear(_ animated: Bool) {
|
|
7093
7177
|
super.viewWillAppear(animated)
|
|
7178
|
+
isLeavingScreen = false
|
|
7094
7179
|
if let navigationBarHidden, navigationController?.isNavigationBarHidden != navigationBarHidden {
|
|
7095
7180
|
navigationController?.setNavigationBarHidden(navigationBarHidden, animated: animated)
|
|
7096
7181
|
} else if navigationBarHidden == nil, navigationController?.isNavigationBarHidden == true,
|
|
@@ -7133,7 +7218,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7133
7218
|
//
|
|
7134
7219
|
// `lastFocusedInput` is deliberately left alone. A screen merely
|
|
7135
7220
|
// covered by a push comes back, and restoreFocus putting the person
|
|
7136
|
-
// back where they were is the behaviour that was already there.
|
|
7221
|
+
// back where they were is the behaviour that was already there. It
|
|
7222
|
+
// is not consulted again until the screen is coming back, or what
|
|
7223
|
+
// is ended here would be started again a moment later.
|
|
7224
|
+
isLeavingScreen = true
|
|
7137
7225
|
view.endEditing(true)
|
|
7138
7226
|
screenVisibilityChanged(false)
|
|
7139
7227
|
stopCelebrations(in: view)
|
|
@@ -7298,6 +7386,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7298
7386
|
pendingCapabilityDeadlines.values.forEach { $0.cancel() }
|
|
7299
7387
|
pendingCapabilityDeadlines.removeAll()
|
|
7300
7388
|
pendingCapabilityRequests.forEach { CraftNativeActions.cancel(requestToken: $0) }
|
|
7389
|
+
CraftNativeActions.endScope(capabilityScope)
|
|
7301
7390
|
pendingCapabilityRequests.removeAll()
|
|
7302
7391
|
if let owner = voiceRecordingOwner {
|
|
7303
7392
|
voiceRecordingOwner = nil
|
|
@@ -7370,6 +7459,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7370
7459
|
emitLayoutEvents()
|
|
7371
7460
|
sendViewportIfChanged()
|
|
7372
7461
|
scheduleKeyboardInsets()
|
|
7462
|
+
scheduleFloatingClearance()
|
|
7373
7463
|
}
|
|
7374
7464
|
|
|
7375
7465
|
/// The width the bundle last heard, so a layout pass at the same size says nothing.
|
|
@@ -8790,6 +8880,13 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
8790
8880
|
}
|
|
8791
8881
|
// The app script has no screen for the keyboard to cover.
|
|
8792
8882
|
if !isAppScript {
|
|
8883
|
+
lifecycleObservers.append(NotificationCenter.default.addObserver(
|
|
8884
|
+
forName: CraftChrome.floatingActionsDidMove,
|
|
8885
|
+
object: nil,
|
|
8886
|
+
queue: .main
|
|
8887
|
+
) { [weak self] _ in
|
|
8888
|
+
self?.scheduleFloatingClearance()
|
|
8889
|
+
})
|
|
8793
8890
|
lifecycleObservers.append(NotificationCenter.default.addObserver(
|
|
8794
8891
|
forName: UIResponder.keyboardWillChangeFrameNotification,
|
|
8795
8892
|
object: nil,
|
|
@@ -8934,7 +9031,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
8934
9031
|
/// took account of) insets its content by that much, so everything in it
|
|
8935
9032
|
/// can be scrolled above the keyboard. Internal for XCTest.
|
|
8936
9033
|
func insetScrollViewsForKeyboard() {
|
|
8937
|
-
var scrolls: [
|
|
9034
|
+
var scrolls: [CraftNativeScrollView] = []
|
|
8938
9035
|
func collect(_ node: RenderedNode?) {
|
|
8939
9036
|
guard let node else { return }
|
|
8940
9037
|
if let scroll = node.view as? CraftNativeScrollView, scroll.isVertical { scrolls.append(scroll) }
|
|
@@ -8951,8 +9048,51 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
8951
9048
|
let automaticBottom = max(0, scroll.adjustedContentInset.bottom - scroll.contentInset.bottom)
|
|
8952
9049
|
overlap = max(0, overlap - automaticBottom)
|
|
8953
9050
|
}
|
|
8954
|
-
|
|
8955
|
-
|
|
9051
|
+
// Its own inset is what is being measured, not part of the answer.
|
|
9052
|
+
scroll.keyboardCover = overlap
|
|
9053
|
+
}
|
|
9054
|
+
}
|
|
9055
|
+
|
|
9056
|
+
/// The compact tab actions float over the trailing end of whatever
|
|
9057
|
+
/// scrolls behind them. Each vertical scroll view and list they reach
|
|
9058
|
+
/// into gets that much room at its end, so its last row can be brought
|
|
9059
|
+
/// clear of them; one that ends above them gets none. How wide a scroll
|
|
9060
|
+
/// view is does not come into it: a page of a pager that is off to one
|
|
9061
|
+
/// side now is under the actions once it is swiped to. The safe area and
|
|
9062
|
+
/// the viewport are left alone: content still scrolls behind the
|
|
9063
|
+
/// actions. Internal for XCTest.
|
|
9064
|
+
func insetScrollViewsForFloatingActions() {
|
|
9065
|
+
let chrome = CraftChrome.shared
|
|
9066
|
+
let actions = chrome.floatingActions.flatMap { frame in
|
|
9067
|
+
chrome.floatingActionsHost.flatMap { view.isDescendant(of: $0) ? frame : nil }
|
|
9068
|
+
}
|
|
9069
|
+
func cover(_ scroll: UIScrollView) -> CGFloat {
|
|
9070
|
+
guard let actions, let window = scroll.window else { return 0 }
|
|
9071
|
+
let frame = scroll.convert(scroll.bounds, to: window)
|
|
9072
|
+
// An edge-to-edge scroller already clears the tab bar through
|
|
9073
|
+
// UIKit's safe area. Add only what the actions reach above it.
|
|
9074
|
+
let automaticBottom = max(0, scroll.adjustedContentInset.bottom - scroll.contentInset.bottom)
|
|
9075
|
+
return max(0, frame.maxY - actions.minY - automaticBottom)
|
|
9076
|
+
}
|
|
9077
|
+
func visit(_ node: RenderedNode?) {
|
|
9078
|
+
guard let node else { return }
|
|
9079
|
+
if let scroll = node.view as? CraftNativeScrollView, scroll.isVertical { scroll.floatingCover = cover(scroll) }
|
|
9080
|
+
else if let list = node.view as? CraftNativeFlatList { list.floatingCover = cover(list) }
|
|
9081
|
+
node.children.forEach(visit)
|
|
9082
|
+
}
|
|
9083
|
+
visit(renderedRoot)
|
|
9084
|
+
floatingClearanceApplied = actions != nil
|
|
9085
|
+
}
|
|
9086
|
+
|
|
9087
|
+
/// A layout moved a view, or the actions moved: measured again once the
|
|
9088
|
+
/// pass is over, where each scroll view now ends.
|
|
9089
|
+
fileprivate func scheduleFloatingClearance() {
|
|
9090
|
+
guard CraftChrome.shared.floatingActions != nil || floatingClearanceApplied, !floatingClearanceScheduled else { return }
|
|
9091
|
+
floatingClearanceScheduled = true
|
|
9092
|
+
DispatchQueue.main.async { [weak self] in
|
|
9093
|
+
guard let self else { return }
|
|
9094
|
+
self.floatingClearanceScheduled = false
|
|
9095
|
+
self.insetScrollViewsForFloatingActions()
|
|
8956
9096
|
}
|
|
8957
9097
|
}
|
|
8958
9098
|
|
|
@@ -9307,7 +9447,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
9307
9447
|
reply(.failure(CraftNativeActionError(code: "UNSUPPORTED_TARGET", message: "Keyboard focus is supported only for text inputs")))
|
|
9308
9448
|
return true
|
|
9309
9449
|
}
|
|
9310
|
-
|
|
9450
|
+
// A screen that is leaving does not open a keyboard it will
|
|
9451
|
+
// not be there to close.
|
|
9452
|
+
guard !isLeavingScreen, node.view.becomeFirstResponder() else {
|
|
9311
9453
|
reply(.failure(CraftNativeActionError(code: "FOCUS_FAILED", message: "The mounted view refused keyboard focus")))
|
|
9312
9454
|
return true
|
|
9313
9455
|
}
|
|
@@ -9652,6 +9794,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
9652
9794
|
}
|
|
9653
9795
|
|
|
9654
9796
|
private func restoreFocus(_ focused: UIView?) {
|
|
9797
|
+
guard !isLeavingScreen else { return }
|
|
9655
9798
|
guard let focused, focused.window != nil, !focused.isFirstResponder,
|
|
9656
9799
|
lastFocusedInput === focused, firstResponder(in: rootStack) == nil else { return }
|
|
9657
9800
|
var ancestor: UIView? = focused
|
|
@@ -9737,12 +9880,12 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
9737
9880
|
case "Text":
|
|
9738
9881
|
let text = children.compactMap { $0 as? String }.joined()
|
|
9739
9882
|
if let selectable = result as? CraftNativeSelectableTextView {
|
|
9740
|
-
selectable.textContainer.maximumNumberOfLines = max(0,
|
|
9883
|
+
selectable.textContainer.maximumNumberOfLines = max(0, craftNativeInt(number(props["numberOfLines"]) ?? 0))
|
|
9741
9884
|
selectable.textContainer.lineBreakMode = textLineBreakMode(props["ellipsizeMode"])
|
|
9742
9885
|
configureSelectableText(selectable, text: text, style: style)
|
|
9743
9886
|
} else {
|
|
9744
9887
|
let label = result as! UILabel
|
|
9745
|
-
label.numberOfLines = max(0,
|
|
9888
|
+
label.numberOfLines = max(0, craftNativeInt(number(props["numberOfLines"]) ?? 0))
|
|
9746
9889
|
label.lineBreakMode = textLineBreakMode(props["ellipsizeMode"])
|
|
9747
9890
|
(label as? CraftNativeLabel)?.insets = paddingInsets(style)
|
|
9748
9891
|
configureText(label, text: text, style: style)
|
|
@@ -9801,6 +9944,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
9801
9944
|
? nil
|
|
9802
9945
|
: inputDrafts[current.identity] ?? props["defaultValue"] as? String
|
|
9803
9946
|
)
|
|
9947
|
+
applyAutoFocus(props["autoFocus"], to: textView, node: current)
|
|
9804
9948
|
break
|
|
9805
9949
|
}
|
|
9806
9950
|
let field = result as! UITextField
|
|
@@ -9819,7 +9963,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
9819
9963
|
field.tintColor = color(props["selectionColor"])
|
|
9820
9964
|
let fieldId = ObjectIdentifier(field)
|
|
9821
9965
|
inputIdentities[fieldId] = current.identity
|
|
9822
|
-
let maxLength =
|
|
9966
|
+
let maxLength = craftNativeInt(number(props["maxLength"]) ?? 0)
|
|
9823
9967
|
if maxLength > 0 {
|
|
9824
9968
|
textMaxLengths[fieldId] = maxLength
|
|
9825
9969
|
} else {
|
|
@@ -9861,9 +10005,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
9861
10005
|
updateAuxiliaryHandler(events["onBlur"], in: &blurHandlers, for: field)
|
|
9862
10006
|
updateAuxiliaryHandler(events["onEndEditing"], in: &endEditingHandlers, for: field)
|
|
9863
10007
|
updateAuxiliaryHandler(events["onSubmitEditing"], in: &submitHandlers, for: field)
|
|
9864
|
-
|
|
9865
|
-
DispatchQueue.main.async { _ = field.becomeFirstResponder() }
|
|
9866
|
-
}
|
|
10008
|
+
applyAutoFocus(props["autoFocus"], to: field, node: current)
|
|
9867
10009
|
case "SegmentedControl":
|
|
9868
10010
|
let control = result as! CraftNativeSegmentedControl
|
|
9869
10011
|
let items = props["items"] as? [[String: Any]] ?? []
|
|
@@ -9932,7 +10074,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
9932
10074
|
if picker.preferredDatePickerStyle != display { picker.preferredDatePickerStyle = display }
|
|
9933
10075
|
}
|
|
9934
10076
|
// UIKit takes an interval that divides an hour, and ignores others.
|
|
9935
|
-
let interval =
|
|
10077
|
+
let interval = craftNativeInt(number(props["minuteInterval"]) ?? 1)
|
|
9936
10078
|
picker.minuteInterval = (1...30).contains(interval) && 60 % interval == 0 ? interval : 1
|
|
9937
10079
|
picker.minimumDate = Self.pickerDate(props["minimumDate"])
|
|
9938
10080
|
picker.maximumDate = Self.pickerDate(props["maximumDate"])
|
|
@@ -9978,11 +10120,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
9978
10120
|
modalFocusRestore[modalID] = focusBeforeContent
|
|
9979
10121
|
}
|
|
9980
10122
|
if visible, wasVisible != true { positionModals() }
|
|
9981
|
-
modal.setVisible(visible, opacity:
|
|
10123
|
+
modal.setVisible(visible, opacity: styleNumber(style["opacity"]) ?? 1, animationType: props["animationType"] as? String)
|
|
9982
10124
|
if !visible, wasVisible == true {
|
|
9983
10125
|
let restore = modalFocusRestore.removeValue(forKey: modalID)
|
|
9984
10126
|
DispatchQueue.main.async { [weak self, weak restore] in
|
|
9985
|
-
guard let self, let restore, restore.window != nil, restore.isDescendant(of: self.view) else { return }
|
|
10127
|
+
guard let self, !self.isLeavingScreen, let restore, restore.window != nil, restore.isDescendant(of: self.view) else { return }
|
|
9986
10128
|
self.lastFocusedInput = restore
|
|
9987
10129
|
_ = restore.becomeFirstResponder()
|
|
9988
10130
|
}
|
|
@@ -10005,7 +10147,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
10005
10147
|
let disabled = props["disabled"] as? Bool == true
|
|
10006
10148
|
pressable.pressActiveOpacity = disabled
|
|
10007
10149
|
? nil : number(props["activeOpacity"]) ?? 0.5
|
|
10008
|
-
pressable.pressBaseOpacity =
|
|
10150
|
+
pressable.pressBaseOpacity = styleNumber(style["opacity"]) ?? 1
|
|
10009
10151
|
pressable.pressUnderlayColor = type == "TouchableHighlight" && !disabled ? color(props["underlayColor"]) : nil
|
|
10010
10152
|
case "Switch":
|
|
10011
10153
|
if let toggle = result as? CraftNativeSolidSwitch {
|
|
@@ -10615,7 +10757,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
10615
10757
|
list.apply(
|
|
10616
10758
|
nodes: children,
|
|
10617
10759
|
horizontal: props["horizontal"] as? Bool == true,
|
|
10618
|
-
columns:
|
|
10760
|
+
columns: craftNativeInt(number(props["numColumns"]) ?? 1),
|
|
10619
10761
|
inverted: props["inverted"] as? Bool == true,
|
|
10620
10762
|
endReachedThreshold: threshold,
|
|
10621
10763
|
renderItem: { [weak self] node, identity, reusable in
|
|
@@ -11148,13 +11290,13 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
11148
11290
|
}
|
|
11149
11291
|
// An input's own scrolling follows its height (CraftNativeTextView).
|
|
11150
11292
|
if !(textView is CraftNativeTextView) { textView.isScrollEnabled = false }
|
|
11151
|
-
let numberOfLines =
|
|
11293
|
+
let numberOfLines = craftNativeInt(number(props["numberOfLines"]) ?? 0)
|
|
11152
11294
|
if numberOfLines > 0 {
|
|
11153
11295
|
textView.textContainer.maximumNumberOfLines = numberOfLines
|
|
11154
11296
|
} else {
|
|
11155
11297
|
textView.textContainer.maximumNumberOfLines = 0
|
|
11156
11298
|
}
|
|
11157
|
-
let maxLength =
|
|
11299
|
+
let maxLength = craftNativeInt(number(props["maxLength"]) ?? 0)
|
|
11158
11300
|
if maxLength > 0 {
|
|
11159
11301
|
textMaxLengths[id] = maxLength
|
|
11160
11302
|
} else {
|
|
@@ -11184,8 +11326,19 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
11184
11326
|
updateAuxiliaryHandler(events["onBlur"], in: &blurHandlers, for: textView)
|
|
11185
11327
|
updateAuxiliaryHandler(events["onEndEditing"], in: &endEditingHandlers, for: textView)
|
|
11186
11328
|
updateAuxiliaryHandler(events["onSubmitEditing"], in: &submitHandlers, for: textView)
|
|
11187
|
-
|
|
11188
|
-
|
|
11329
|
+
}
|
|
11330
|
+
|
|
11331
|
+
/// `autoFocus`: focus when the input mounts with it, or when it is turned
|
|
11332
|
+
/// on later. Applied on every render it took the keyboard back each time
|
|
11333
|
+
/// anything else on the screen changed (#511).
|
|
11334
|
+
private func applyAutoFocus(_ value: Any?, to input: UIView, node: RenderedNode) {
|
|
11335
|
+
let wanted = value as? Bool == true
|
|
11336
|
+
let turnedOn = wanted && !node.autoFocus
|
|
11337
|
+
node.autoFocus = wanted
|
|
11338
|
+
guard turnedOn, !input.isFirstResponder else { return }
|
|
11339
|
+
DispatchQueue.main.async { [weak self] in
|
|
11340
|
+
guard self?.isLeavingScreen == false else { return }
|
|
11341
|
+
_ = input.becomeFirstResponder()
|
|
11189
11342
|
}
|
|
11190
11343
|
}
|
|
11191
11344
|
|
|
@@ -11195,8 +11348,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
11195
11348
|
/// leaving a hole.
|
|
11196
11349
|
private func configureIcon(_ icon: UIImageView, props: [String: Any], style: [String: Any]) {
|
|
11197
11350
|
let name = (props["symbol"] as? String) ?? (props["name"] as? String) ?? ""
|
|
11198
|
-
let box = [
|
|
11199
|
-
let fontSize =
|
|
11351
|
+
let box = [styleNumber(style["width"]), styleNumber(style["height"])].compactMap { $0 }.min()
|
|
11352
|
+
let fontSize = styleNumber(style["fontSize"])
|
|
11200
11353
|
// Without a size of its own an icon is a 20 pt square (contract A2).
|
|
11201
11354
|
(icon as? CraftNativeIconView)?.defaultBox = box == nil && fontSize == nil ? 20 : nil
|
|
11202
11355
|
let pointSize = fontSize ?? (box ?? 20) * 0.84
|
|
@@ -11237,10 +11390,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
11237
11390
|
stack.alignItems = style["alignItems"] as? String ?? "stretch"
|
|
11238
11391
|
stack.alignContent = style["alignContent"] as? String ?? "flex-start"
|
|
11239
11392
|
stack.justifyContent = style["justifyContent"] as? String ?? "flex-start"
|
|
11240
|
-
let gap = max(0,
|
|
11393
|
+
let gap = max(0, styleNumber(style["gap"]) ?? 0)
|
|
11241
11394
|
stack.gap = gap
|
|
11242
|
-
stack.rowGap = max(0,
|
|
11243
|
-
stack.columnGap = max(0,
|
|
11395
|
+
stack.rowGap = max(0, styleNumber(style["rowGap"]) ?? gap)
|
|
11396
|
+
stack.columnGap = max(0, styleNumber(style["columnGap"]) ?? gap)
|
|
11244
11397
|
stack.grid = style["display"] as? String == "grid"
|
|
11245
11398
|
stack.gridColumns = craftNativeGridColumnCount(style["gridTemplateColumns"] ?? style["gridColumns"])
|
|
11246
11399
|
stack.gridRows = craftNativeGridRowCount(style["gridTemplateRows"])
|
|
@@ -11252,8 +11405,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
11252
11405
|
private func addJustificationSpacers(to stack: CraftNativeFlowView, value: String?) { }
|
|
11253
11406
|
|
|
11254
11407
|
private func applyViewStyle(_ style: [String: Any], to view: UIView, node: RenderedNode) {
|
|
11255
|
-
let width = style["minWidth"] == nil && style["maxWidth"] == nil ?
|
|
11256
|
-
let height = style["minHeight"] == nil && style["maxHeight"] == nil ?
|
|
11408
|
+
let width = style["minWidth"] == nil && style["maxWidth"] == nil ? styleNumber(style["width"]) : nil
|
|
11409
|
+
let height = style["minHeight"] == nil && style["maxHeight"] == nil ? styleNumber(style["height"]) : nil
|
|
11257
11410
|
updateDimension(width, constraint: &node.widthConstraint, anchor: view.widthAnchor)
|
|
11258
11411
|
updateDimension(height, constraint: &node.heightConstraint, anchor: view.heightAnchor)
|
|
11259
11412
|
view.backgroundColor = color(style["backgroundColor"]) ?? .clear
|
|
@@ -11272,31 +11425,31 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
11272
11425
|
direction: style["gradientDirection"] as? String,
|
|
11273
11426
|
colors: [color(style["gradientFrom"]), color(style["gradientVia"]), color(style["gradientTo"])]
|
|
11274
11427
|
)
|
|
11275
|
-
view.alpha =
|
|
11428
|
+
view.alpha = styleNumber(style["opacity"]) ?? 1
|
|
11276
11429
|
view.isHidden = style["display"] as? String == "none"
|
|
11277
|
-
let borderRadius =
|
|
11430
|
+
let borderRadius = styleNumber(style["borderRadius"]) ?? 0
|
|
11278
11431
|
let cornerRadiusKeys = ["borderTopLeftRadius", "borderTopRightRadius", "borderBottomRightRadius", "borderBottomLeftRadius"]
|
|
11279
11432
|
let hasCornerRadii = cornerRadiusKeys.contains { style[$0] != nil }
|
|
11280
11433
|
view.craftRequestedCornerRadius = borderRadius
|
|
11281
11434
|
view.craftRequestedCornerRadii = hasCornerRadii
|
|
11282
|
-
? cornerRadiusKeys.map {
|
|
11435
|
+
? cornerRadiusKeys.map { styleNumber(style[$0]) ?? borderRadius }
|
|
11283
11436
|
: nil
|
|
11284
|
-
let borderWidth =
|
|
11437
|
+
let borderWidth = styleNumber(style["borderWidth"]) ?? 0
|
|
11285
11438
|
let borderColor = color(style["borderColor"]) ?? .clear
|
|
11286
11439
|
let sideWidthKeys = ["borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth"]
|
|
11287
11440
|
let sideColorKeys = ["borderTopColor", "borderRightColor", "borderBottomColor", "borderLeftColor"]
|
|
11288
11441
|
view.craftApplySideBorders(
|
|
11289
11442
|
(sideWidthKeys + sideColorKeys).contains { style[$0] != nil },
|
|
11290
11443
|
style: style["borderStyle"] as? String,
|
|
11291
|
-
widths: sideWidthKeys.map {
|
|
11444
|
+
widths: sideWidthKeys.map { styleNumber(style[$0]) ?? borderWidth },
|
|
11292
11445
|
colors: sideColorKeys.map { color(style[$0]) ?? borderColor }
|
|
11293
11446
|
)
|
|
11294
|
-
view.layer.zPosition =
|
|
11447
|
+
view.layer.zPosition = styleNumber(style["zIndex"]) ?? 0
|
|
11295
11448
|
applyTransform(style["transform"], to: view)
|
|
11296
|
-
let elevation = max(0,
|
|
11449
|
+
let elevation = max(0, styleNumber(style["elevation"]) ?? 0)
|
|
11297
11450
|
let shadowColor = color(style["shadowColor"]) ?? .black
|
|
11298
|
-
let shadowOpacity = min(1, max(0,
|
|
11299
|
-
let shadowRadius = max(0,
|
|
11451
|
+
let shadowOpacity = min(1, max(0, styleNumber(style["shadowOpacity"]) ?? (elevation > 0 ? min(0.28, 0.12 + elevation * 0.02) : 0)))
|
|
11452
|
+
let shadowRadius = max(0, styleNumber(style["shadowRadius"]) ?? elevation * 0.5)
|
|
11300
11453
|
let shadowOffset = style["shadowOffset"] as? [String: Any]
|
|
11301
11454
|
view.layer.shadowColor = shadowColor.cgColor
|
|
11302
11455
|
view.layer.shadowOpacity = Float(shadowOpacity)
|
|
@@ -11329,7 +11482,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
11329
11482
|
|
|
11330
11483
|
private func paddingInsets(_ style: [String: Any]) -> UIEdgeInsets {
|
|
11331
11484
|
func nonNegative(_ value: Any?) -> CGFloat? {
|
|
11332
|
-
guard let value =
|
|
11485
|
+
guard let value = styleNumber(value), value >= 0 else { return nil }
|
|
11333
11486
|
return value
|
|
11334
11487
|
}
|
|
11335
11488
|
let padding = nonNegative(style["padding"]) ?? 0
|
|
@@ -11352,11 +11505,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
11352
11505
|
var threeDimensional = CATransform3DIdentity
|
|
11353
11506
|
var hasThreeDimensionalRotation = false
|
|
11354
11507
|
for transform in transforms {
|
|
11355
|
-
if let value =
|
|
11356
|
-
if let value =
|
|
11357
|
-
if let value =
|
|
11358
|
-
if let value =
|
|
11359
|
-
if let value =
|
|
11508
|
+
if let value = styleNumber(transform["translateX"]) { affine = affine.translatedBy(x: value, y: 0) }
|
|
11509
|
+
if let value = styleNumber(transform["translateY"]) { affine = affine.translatedBy(x: 0, y: value) }
|
|
11510
|
+
if let value = styleNumber(transform["scale"]) { affine = affine.scaledBy(x: value, y: value) }
|
|
11511
|
+
if let value = styleNumber(transform["scaleX"]) { affine = affine.scaledBy(x: value, y: 1) }
|
|
11512
|
+
if let value = styleNumber(transform["scaleY"]) { affine = affine.scaledBy(x: 1, y: value) }
|
|
11360
11513
|
if let value = transformAngle(transform["rotate"] ?? transform["rotateZ"]) { affine = affine.rotated(by: value) }
|
|
11361
11514
|
if let value = transformAngle(transform["skewX"]) { affine = affine.concatenating(CGAffineTransform(a: 1, b: 0, c: tan(value), d: 1, tx: 0, ty: 0)) }
|
|
11362
11515
|
if let value = transformAngle(transform["skewY"]) { affine = affine.concatenating(CGAffineTransform(a: 1, b: tan(value), c: 0, d: 1, tx: 0, ty: 0)) }
|
|
@@ -11394,10 +11547,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
11394
11547
|
label.font = textFont(style, default: .systemFont(ofSize: 16))
|
|
11395
11548
|
label.textAlignment = textAlignment(style["textAlign"])
|
|
11396
11549
|
var attributes: [NSAttributedString.Key: Any] = [:]
|
|
11397
|
-
if let spacing =
|
|
11550
|
+
if let spacing = styleNumber(style["letterSpacing"]) { attributes[.kern] = spacing }
|
|
11398
11551
|
addTextDecorationAttributes(style, to: &attributes)
|
|
11399
11552
|
if let shadow = textShadow(style) { attributes[.shadow] = shadow }
|
|
11400
|
-
if let lineHeight =
|
|
11553
|
+
if let lineHeight = styleNumber(style["lineHeight"]) {
|
|
11401
11554
|
let paragraph = NSMutableParagraphStyle()
|
|
11402
11555
|
paragraph.minimumLineHeight = lineHeight
|
|
11403
11556
|
paragraph.maximumLineHeight = lineHeight
|
|
@@ -11451,8 +11604,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
11451
11604
|
.foregroundColor: color,
|
|
11452
11605
|
.font: font,
|
|
11453
11606
|
]
|
|
11454
|
-
if let spacing =
|
|
11455
|
-
if let lineHeight =
|
|
11607
|
+
if let spacing = styleNumber(style["letterSpacing"]) { attributes[.kern] = spacing }
|
|
11608
|
+
if let lineHeight = styleNumber(style["lineHeight"]) {
|
|
11456
11609
|
let paragraph = NSMutableParagraphStyle()
|
|
11457
11610
|
paragraph.minimumLineHeight = lineHeight
|
|
11458
11611
|
paragraph.maximumLineHeight = lineHeight
|
|
@@ -11474,14 +11627,14 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
11474
11627
|
.foregroundColor: color(style["color"]) ?? .label,
|
|
11475
11628
|
.font: font,
|
|
11476
11629
|
]
|
|
11477
|
-
if let spacing =
|
|
11630
|
+
if let spacing = styleNumber(style["letterSpacing"]) { attributes[.kern] = spacing }
|
|
11478
11631
|
// UIKit otherwise resets the field's alignment to natural when these attributes are applied.
|
|
11479
11632
|
let paragraph = NSMutableParagraphStyle()
|
|
11480
11633
|
paragraph.alignment = alignment
|
|
11481
11634
|
// The default paragraph wraps even inside a UITextField. A long URL
|
|
11482
11635
|
// then paints a second line outside the field instead of scrolling.
|
|
11483
11636
|
if singleLine { paragraph.lineBreakMode = .byClipping }
|
|
11484
|
-
if let lineHeight =
|
|
11637
|
+
if let lineHeight = styleNumber(style["lineHeight"]) {
|
|
11485
11638
|
paragraph.minimumLineHeight = lineHeight
|
|
11486
11639
|
paragraph.maximumLineHeight = lineHeight
|
|
11487
11640
|
}
|
|
@@ -11500,7 +11653,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
11500
11653
|
width: number(offset?["width"]) ?? 0,
|
|
11501
11654
|
height: number(offset?["height"]) ?? 0
|
|
11502
11655
|
)
|
|
11503
|
-
shadow.shadowBlurRadius = max(0,
|
|
11656
|
+
shadow.shadowBlurRadius = max(0, styleNumber(style["textShadowRadius"]) ?? 0)
|
|
11504
11657
|
return shadow
|
|
11505
11658
|
}
|
|
11506
11659
|
|
|
@@ -11539,7 +11692,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
11539
11692
|
}
|
|
11540
11693
|
|
|
11541
11694
|
private func textFont(_ style: [String: Any], default fallback: UIFont) -> UIFont {
|
|
11542
|
-
let size =
|
|
11695
|
+
let size = styleNumber(style["fontSize"]) ?? fallback.pointSize
|
|
11543
11696
|
let weight: UIFont.Weight
|
|
11544
11697
|
switch craftNativeFontWeight(style["fontWeight"]) {
|
|
11545
11698
|
case "100": weight = .ultraLight
|