@craft-native/ios 0.0.134 → 0.0.136
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 +7 -0
- package/package.json +1 -1
- package/templates/CraftApp.swift +67 -2
- package/templates/CraftHybrid.swift +28 -2
package/README.md
CHANGED
|
@@ -505,6 +505,13 @@ await window.craft.restorePurchases();
|
|
|
505
505
|
window.craft.setKeepAwake(true); // Prevent screen dimming
|
|
506
506
|
window.craft.setKeepAwake(false); // Allow screen dimming
|
|
507
507
|
|
|
508
|
+
// Screen brightness, 0 to 1: always available (no config flag). It is the
|
|
509
|
+
// device's backlight, so put it back: `brightness` from 'craft-native/mobile'
|
|
510
|
+
// remembers the person's level, restores it, and releases it while the app is
|
|
511
|
+
// in the background.
|
|
512
|
+
const level = await window.craft.getBrightness(); // 0.62
|
|
513
|
+
await window.craft.setBrightness(0.1); // resolves with what the screen reports
|
|
514
|
+
|
|
508
515
|
// Speech: short spoken cues that duck the person's music instead of stopping
|
|
509
516
|
// it, and play with the silent switch on. Always available (no config flag).
|
|
510
517
|
// Resolves when the utterance ends: true if spoken, false if stopped or
|
package/package.json
CHANGED
package/templates/CraftApp.swift
CHANGED
|
@@ -535,8 +535,25 @@ final class CraftChrome: ObservableObject {
|
|
|
535
535
|
private var occupied: CGFloat = 0
|
|
536
536
|
private var pendingHide: DispatchWorkItem?
|
|
537
537
|
private var splashDeadline: DispatchWorkItem?
|
|
538
|
+
/// The keyboard is up and covers the tab bar, so the page need not leave
|
|
539
|
+
/// room for it: the web view ends at the keyboard (CraftHybrid's webHost).
|
|
540
|
+
private var keyboardUp = false
|
|
538
541
|
|
|
539
|
-
private init() {
|
|
542
|
+
private init() {
|
|
543
|
+
let center = NotificationCenter.default
|
|
544
|
+
center.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { [weak self] _ in
|
|
545
|
+
self?.keyboardChanged(up: true)
|
|
546
|
+
}
|
|
547
|
+
center.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { [weak self] _ in
|
|
548
|
+
self?.keyboardChanged(up: false)
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private func keyboardChanged(up: Bool) {
|
|
553
|
+
guard up != keyboardUp else { return }
|
|
554
|
+
keyboardUp = up
|
|
555
|
+
publishLayout()
|
|
556
|
+
}
|
|
540
557
|
|
|
541
558
|
/// The tab bar from `tabs` in craft.config.json, there before the page
|
|
542
559
|
/// loads; the page's own `tabBar` message replaces it later. `splash`
|
|
@@ -685,7 +702,7 @@ final class CraftChrome: ObservableObject {
|
|
|
685
702
|
/// The bar's height as `--craft-tab-bar-height`, so the page leaves room
|
|
686
703
|
/// for it, and as an event for anything that measures.
|
|
687
704
|
func publishLayout() {
|
|
688
|
-
let height = tabBarVisible ? Int(occupied) : 0
|
|
705
|
+
let height = tabBarVisible && !keyboardUp ? Int(occupied) : 0
|
|
689
706
|
CraftHybrid.shared.tabBarCovers(CGFloat(height))
|
|
690
707
|
let script = "document.documentElement.style.setProperty('--craft-tab-bar-height','\(height)px');window.dispatchEvent(new CustomEvent('craftTabBarLayout',{detail:{height:\(height)}}));"
|
|
691
708
|
DispatchQueue.main.async { [weak self] in self?.webView?.evaluateJavaScript(script, completionHandler: nil) }
|
|
@@ -2140,6 +2157,15 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
2140
2157
|
} else {
|
|
2141
2158
|
rejectCallback(callbackId, error: "Keep awake is disabled", code: "CAPABILITY_DISABLED")
|
|
2142
2159
|
}
|
|
2160
|
+
// MARK: - Screen Brightness
|
|
2161
|
+
case "getBrightness":
|
|
2162
|
+
getBrightness(callbackId: callbackId)
|
|
2163
|
+
case "setBrightness":
|
|
2164
|
+
if let level = (body["level"] as? NSNumber)?.doubleValue, level.isFinite {
|
|
2165
|
+
setBrightness(level, callbackId: callbackId)
|
|
2166
|
+
} else {
|
|
2167
|
+
rejectCallback(callbackId, error: "setBrightness was called without the values it needs", code: "INVALID_ARGUMENT")
|
|
2168
|
+
}
|
|
2143
2169
|
// MARK: - Orientation Lock
|
|
2144
2170
|
case "lockOrientation":
|
|
2145
2171
|
if config.enableOrientationLock {
|
|
@@ -3102,6 +3128,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
3102
3128
|
orientationLock: \(config.enableOrientationLock),
|
|
3103
3129
|
deepLinks: \(config.enableDeepLinks),
|
|
3104
3130
|
flashlight: true,
|
|
3131
|
+
brightness: true,
|
|
3105
3132
|
speech: true,
|
|
3106
3133
|
network: true,
|
|
3107
3134
|
deviceInfo: true,
|
|
@@ -4041,6 +4068,25 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
4041
4068
|
});
|
|
4042
4069
|
},
|
|
4043
4070
|
|
|
4071
|
+
// Screen brightness, 0 to 1. Each resolves with the level the
|
|
4072
|
+
// screen then reports.
|
|
4073
|
+
getBrightness: function() {
|
|
4074
|
+
var self = this;
|
|
4075
|
+
var id = 'cb_' + (++this._callbackId);
|
|
4076
|
+
window.webkit.messageHandlers.craft.postMessage({action: 'getBrightness', callbackId: id});
|
|
4077
|
+
return new Promise(function(resolve, reject) {
|
|
4078
|
+
self._callbacks[id] = {resolve: resolve, reject: reject};
|
|
4079
|
+
});
|
|
4080
|
+
},
|
|
4081
|
+
setBrightness: function(level) {
|
|
4082
|
+
var self = this;
|
|
4083
|
+
var id = 'cb_' + (++this._callbackId);
|
|
4084
|
+
window.webkit.messageHandlers.craft.postMessage({action: 'setBrightness', level: level, callbackId: id});
|
|
4085
|
+
return new Promise(function(resolve, reject) {
|
|
4086
|
+
self._callbacks[id] = {resolve: resolve, reject: reject};
|
|
4087
|
+
});
|
|
4088
|
+
},
|
|
4089
|
+
|
|
4044
4090
|
// Speech synthesis. speak() settles when the utterance ends:
|
|
4045
4091
|
// true when it was spoken, false when something stopped it.
|
|
4046
4092
|
// No timeout, because a long sentence at a slow rate can
|
|
@@ -6236,6 +6282,25 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
6236
6282
|
}
|
|
6237
6283
|
}
|
|
6238
6284
|
|
|
6285
|
+
// MARK: - Screen Brightness
|
|
6286
|
+
//
|
|
6287
|
+
// The device's backlight, 0 to 1. It is system-wide, not the app's:
|
|
6288
|
+
// a page that dims it reads it first and puts it back - when it is
|
|
6289
|
+
// done, and when the app leaves the foreground (@stacksjs/mobile's
|
|
6290
|
+
// screen API does both off the page's visibility).
|
|
6291
|
+
private func getBrightness(callbackId: String?) {
|
|
6292
|
+
DispatchQueue.main.async {
|
|
6293
|
+
self.resolveCallback(callbackId, result: Double(UIScreen.main.brightness))
|
|
6294
|
+
}
|
|
6295
|
+
}
|
|
6296
|
+
|
|
6297
|
+
private func setBrightness(_ level: Double, callbackId: String?) {
|
|
6298
|
+
DispatchQueue.main.async {
|
|
6299
|
+
UIScreen.main.brightness = CGFloat(min(max(level, 0), 1))
|
|
6300
|
+
self.resolveCallback(callbackId, result: Double(UIScreen.main.brightness))
|
|
6301
|
+
}
|
|
6302
|
+
}
|
|
6303
|
+
|
|
6239
6304
|
// MARK: - Speech Synthesis
|
|
6240
6305
|
//
|
|
6241
6306
|
// Native rather than the page's own `speechSynthesis`, which in a
|
|
@@ -539,6 +539,14 @@ final class CraftHybrid: NSObject {
|
|
|
539
539
|
}, function () { arriving = null; finish(); });
|
|
540
540
|
});
|
|
541
541
|
},
|
|
542
|
+
// Put the page at `path` in place of where it is, without a step in
|
|
543
|
+
// its history: the shell is back at a native screen and the page
|
|
544
|
+
// behind it had no history left to go back through (it reloaded).
|
|
545
|
+
reset: function (path) {
|
|
546
|
+
if (!(navigate && current && current.__craftHybrid)) { location.replace(path); return Promise.resolve(false); }
|
|
547
|
+
hostNav = path;
|
|
548
|
+
return Promise.resolve(navigate(path, { instant: true, replace: true }));
|
|
549
|
+
},
|
|
542
550
|
setBase: function (value) { base = typeof value === 'number' ? value : null; },
|
|
543
551
|
// Back to `target` in the page's stack, after the shell popped the
|
|
544
552
|
// entries above it.
|
|
@@ -667,6 +675,11 @@ final class CraftHybrid: NSObject {
|
|
|
667
675
|
webView?.evaluateJavaScript("window.__craftHybrid && window.__craftHybrid.setBase(\(value))", completionHandler: nil)
|
|
668
676
|
}
|
|
669
677
|
|
|
678
|
+
func pageReset(to path: String) {
|
|
679
|
+
guard let data = try? JSONSerialization.data(withJSONObject: [path]), let json = String(data: data, encoding: .utf8) else { return }
|
|
680
|
+
webView?.evaluateJavaScript("window.__craftHybrid && window.__craftHybrid.reset && window.__craftHybrid.reset(\(json)[0])", completionHandler: nil)
|
|
681
|
+
}
|
|
682
|
+
|
|
670
683
|
func pageBack(to depth: Int) {
|
|
671
684
|
webView?.evaluateJavaScript("window.__craftHybrid && window.__craftHybrid.backTo(\(max(0, depth)))", completionHandler: nil)
|
|
672
685
|
}
|
|
@@ -752,7 +765,10 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
752
765
|
|
|
753
766
|
init(config: CraftConfig) {
|
|
754
767
|
self.config = config
|
|
755
|
-
|
|
768
|
+
// Edge to edge, but not under the keyboard: the web view ends at its
|
|
769
|
+
// top. Under it, WebKit scrolled the page's layout viewport to show a
|
|
770
|
+
// focused field and carried every sticky header off the screen.
|
|
771
|
+
webHost = UIHostingController(rootView: AnyView(CraftWebView(config: config).ignoresSafeArea(.container)))
|
|
756
772
|
super.init(nibName: nil, bundle: nil)
|
|
757
773
|
hybrid.controller = self
|
|
758
774
|
}
|
|
@@ -784,7 +800,7 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
784
800
|
guard !webAttached else { return }
|
|
785
801
|
webAttached = true
|
|
786
802
|
webHost.view.backgroundColor = config.resolvedBackgroundColor
|
|
787
|
-
if #available(iOS 16.4, *) { webHost.safeAreaRegions = [] }
|
|
803
|
+
if #available(iOS 16.4, *) { webHost.safeAreaRegions = [.keyboard] }
|
|
788
804
|
if let slot = visibleStack?.topViewController as? CraftHybridWebSlot, slot.isViewLoaded {
|
|
789
805
|
moveWeb(to: slot, in: slot.view)
|
|
790
806
|
} else {
|
|
@@ -1109,6 +1125,16 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1109
1125
|
if let depth = removed.compactMap({ ($0 as? CraftHybridWebSlot)?.depth }).min() {
|
|
1110
1126
|
hybrid.pageBack(to: depth - 1)
|
|
1111
1127
|
}
|
|
1128
|
+
// Back at a tab's native root while the page, with no history left
|
|
1129
|
+
// to go back through (it reloaded on the screen just popped), still
|
|
1130
|
+
// shows that screen: it moves to the root's path. Left there, the
|
|
1131
|
+
// next screen opened from the root was pushed on top of it and named
|
|
1132
|
+
// it in its Back button ("Me" instead of "Today").
|
|
1133
|
+
if !removed.isEmpty, !(viewController is CraftHybridWebSlot), navigationController.viewControllers.first === viewController,
|
|
1134
|
+
let rootPath = nativePaths[ObjectIdentifier(viewController)], !rootPath.contains(":"), hybrid.pageDepth == 0,
|
|
1135
|
+
let pagePath = hybrid.pagePath, CraftHybridRoutes.normalize(pagePath) != CraftHybridRoutes.normalize(rootPath) {
|
|
1136
|
+
hybrid.pageReset(to: rootPath)
|
|
1137
|
+
}
|
|
1112
1138
|
guard navigationController === visibleStack else { return }
|
|
1113
1139
|
if let slot = viewController as? CraftHybridWebSlot { adoptWeb(into: slot) } else { parkWeb() }
|
|
1114
1140
|
}
|