@swiftbrowser/native 0.17.3 → 0.17.5
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 +5 -2
- package/dist/bridge.d.ts +15 -2
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js.map +1 -1
- package/dist/image.d.ts +55 -0
- package/dist/image.d.ts.map +1 -0
- package/dist/image.js +121 -0
- package/dist/image.js.map +1 -0
- package/dist/runtime.d.ts +5 -3
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +13 -3
- package/dist/runtime.js.map +1 -1
- package/package.json +3 -3
- package/src/bridge.ts +16 -2
- package/src/image.ts +131 -0
- package/src/runtime.ts +14 -3
- package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +17 -0
- package/swift/Sources/SwiftBrowserNative/Host/ShimAliases.swift +2 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/ImageLoader.swift +331 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/RemoteImage.swift +106 -9
- package/wasm/SwiftBrowserNative.wasm +2 -2
package/README.md
CHANGED
|
@@ -13,8 +13,11 @@ three things:
|
|
|
13
13
|
`console`, `performance`, `fetch` with `Headers`, `Request`, `Response`
|
|
14
14
|
and `AbortController`, `URL`, `URLSearchParams`, `TextEncoder`,
|
|
15
15
|
`TextDecoder`, `atob`, `btoa`, `crypto.getRandomValues` and `randomUUID`,
|
|
16
|
-
`localStorage`, `structuredClone`, `navigator`
|
|
17
|
-
|
|
16
|
+
`localStorage`, `structuredClone`, `navigator`, `Image` over the host's
|
|
17
|
+
image loader, for the size of a picture without drawing it, which is how
|
|
18
|
+
`AsyncImage`'s `placeholder` and `failure` show natively). React's
|
|
19
|
+
scheduler reads some of these when it loads, so the bundle starts with the
|
|
20
|
+
runtime.
|
|
18
21
|
- **The bundler**: `swiftbrowser-native build` writes one `app.js` for the
|
|
19
22
|
Xcode target, and `swiftbrowser-native dev` serves it with rebuild on
|
|
20
23
|
save for the app's `NativeApp.watch(url:)`.
|
package/dist/bridge.d.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* The native side of the bridge, as Swift installs it on `__sbNative`
|
|
3
3
|
* (Native/Sources/SwiftBrowserNative/Host/HostBridge.swift). Every call is
|
|
4
4
|
* synchronous; the ones that answer later do so by calling a global this
|
|
5
|
-
* package defines (`__sbTimerFired`, `__sbFetchResult`, `
|
|
6
|
-
* `__sbOpenURL`).
|
|
5
|
+
* package defines (`__sbTimerFired`, `__sbFetchResult`, `__sbImageProbed`,
|
|
6
|
+
* `__sbWorkerMessage`, `__sbOpenURL`).
|
|
7
7
|
*/
|
|
8
8
|
export interface NativeBridge {
|
|
9
9
|
/** Applies a render pass's ops (a JSON array). Returns an error message, or nothing. */
|
|
@@ -15,6 +15,14 @@ export interface NativeBridge {
|
|
|
15
15
|
clearTimer(id: number): void;
|
|
16
16
|
/** Starts a request; the answer arrives at `__sbFetchResult(id, result)`. */
|
|
17
17
|
fetch(id: number, request: string | NativeFetchRequest): void;
|
|
18
|
+
/**
|
|
19
|
+
* Starts loading the image at `url` for its pixel size (image.ts: the
|
|
20
|
+
* runtime's `Image`), keeping the bytes for the `image` element that
|
|
21
|
+
* follows; the answer arrives at `__sbImageProbed(id, result)`. Hosts
|
|
22
|
+
* before 0.18 lack it, and so does the page's host for the round trip,
|
|
23
|
+
* which has a real `Image`.
|
|
24
|
+
*/
|
|
25
|
+
probeImage?(id: number, url: string): void;
|
|
18
26
|
storageGet(key: string): string | null;
|
|
19
27
|
storageSet(key: string, value: string): void;
|
|
20
28
|
storageRemove(key: string): void;
|
|
@@ -56,6 +64,11 @@ export interface NativeFetchRequest {
|
|
|
56
64
|
/** Base64, or null. */
|
|
57
65
|
body: string | null;
|
|
58
66
|
}
|
|
67
|
+
export interface NativeImageProbeResult {
|
|
68
|
+
width?: number;
|
|
69
|
+
height?: number;
|
|
70
|
+
error?: string;
|
|
71
|
+
}
|
|
59
72
|
export interface NativeFetchResult {
|
|
60
73
|
status?: number;
|
|
61
74
|
statusText?: string;
|
package/dist/bridge.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,wFAAwF;IACxF,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAClD,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,+BAA+B;IAC/B,GAAG,IAAI,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IAC9D,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,6EAA6E;IAC7E,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAAC;IAC9D,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACvC,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,WAAW,IAAI,MAAM,EAAE,CAAC;IACxB,YAAY,IAAI,IAAI,CAAC;IACrB,sCAAsC;IACtC,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACnC,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC9B,gDAAgD;IAChD,MAAM,IAAI,MAAM,CAAC;IACjB,+FAA+F;IAC/F,eAAe,CAAC,IAAI,MAAM,EAAE,CAAC;IAM7B;;;;;OAKG;IACH,WAAW,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,YAAY,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACzF,0FAA0F;IAC1F,UAAU,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,eAAe,CAAC,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,sEAAsE;IACtE,iBAAiB,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACxC;AAED;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,SAAS,CAAC;AAEjD,kEAAkE;AAClE,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAG1C;AAED,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,uBAAuB;IACvB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,yFAAyF;AACzF,wBAAgB,YAAY,CAAC,CAAC,GAAE,OAAO,UAAuB,GAAG,YAAY,GAAG,SAAS,CAExF"}
|
|
1
|
+
{"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"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"}
|
package/dist/bridge.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAgEA,kEAAkE;AAClE,MAAM,UAAU,eAAe;IAC7B,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;IAC9B,OAAO,MAAM,EAAE,eAAe,EAAE,EAAE,IAAI,EAAE,CAAC;AAC3C,CAAC;AA0BD,yFAAyF;AACzF,MAAM,UAAU,YAAY,CAAC,IAAuB,UAAU;IAC5D,OAAQ,CAA8C,CAAC,UAAU,CAAC;AACpE,CAAC"}
|
package/dist/image.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Image` over the bridge's `probeImage`: what a page's `new Image()` gives
|
|
3
|
+
* code that loads a picture to learn its size without drawing it (React's
|
|
4
|
+
* `AsyncImage`, which shows its placeholder meanwhile). Setting `src` starts
|
|
5
|
+
* the probe; `onload` (or a `load` listener) comes with `naturalWidth` and
|
|
6
|
+
* `naturalHeight`, `onerror` when the image could not be loaded or read.
|
|
7
|
+
* Swift fetches the bytes through its image loader, so the `image` element
|
|
8
|
+
* that follows draws from the cache. Nothing else of an `HTMLImageElement`
|
|
9
|
+
* is here: no drawing, no DOM.
|
|
10
|
+
*/
|
|
11
|
+
import type { NativeBridge, NativeImageProbeResult } from './bridge';
|
|
12
|
+
interface ImageEvent {
|
|
13
|
+
type: 'load' | 'error';
|
|
14
|
+
target: SimpleImage;
|
|
15
|
+
}
|
|
16
|
+
type Listener = (event: ImageEvent) => void;
|
|
17
|
+
/** Binds `Image` to `native`'s `probeImage`; returns the globals to install. */
|
|
18
|
+
export declare function createImageGlobals(native: NativeBridge): {
|
|
19
|
+
Image: typeof SimpleImage;
|
|
20
|
+
__sbImageProbed: typeof receiveImageProbe;
|
|
21
|
+
};
|
|
22
|
+
/** The bridge's `__sbImageProbed(id, result)`. */
|
|
23
|
+
export declare function receiveImageProbe(id: number, result: NativeImageProbeResult | string): void;
|
|
24
|
+
export declare class SimpleImage {
|
|
25
|
+
naturalWidth: number;
|
|
26
|
+
naturalHeight: number;
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
/** True once the load settled, either way, as on the web. */
|
|
30
|
+
complete: boolean;
|
|
31
|
+
onload: Listener | null;
|
|
32
|
+
onerror: Listener | null;
|
|
33
|
+
crossOrigin: string | null;
|
|
34
|
+
decoding: string;
|
|
35
|
+
loading: string;
|
|
36
|
+
referrerPolicy: string;
|
|
37
|
+
alt: string;
|
|
38
|
+
private source;
|
|
39
|
+
private probe;
|
|
40
|
+
private readonly listeners;
|
|
41
|
+
private waiting;
|
|
42
|
+
constructor(width?: number, height?: number);
|
|
43
|
+
get src(): string;
|
|
44
|
+
/** Starts loading; an earlier load still under way is forgotten. */
|
|
45
|
+
set src(value: string);
|
|
46
|
+
addEventListener(type: string, listener: Listener): void;
|
|
47
|
+
removeEventListener(type: string, listener: Listener): void;
|
|
48
|
+
/** Resolves once the image loaded, rejects when it could not. */
|
|
49
|
+
decode(): Promise<void>;
|
|
50
|
+
/** @internal The bridge's answer. */
|
|
51
|
+
settle(result: NativeImageProbeResult): void;
|
|
52
|
+
private dispatch;
|
|
53
|
+
}
|
|
54
|
+
export {};
|
|
55
|
+
//# sourceMappingURL=image.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../src/image.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAErE,UAAU,UAAU;IAClB,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IACvB,MAAM,EAAE,WAAW,CAAC;CACrB;AACD,KAAK,QAAQ,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;AAM5C,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG;IAAE,KAAK,EAAE,OAAO,WAAW,CAAC;IAAC,eAAe,EAAE,OAAO,iBAAiB,CAAA;CAAE,CAKjI;AAED,kDAAkD;AAClD,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,sBAAsB,GAAG,MAAM,GAAG,IAAI,CAK3F;AAED,qBAAa,WAAW;IACtB,YAAY,SAAK;IACjB,aAAa,SAAK;IAClB,KAAK,SAAK;IACV,MAAM,SAAK;IACX,6DAA6D;IAC7D,QAAQ,UAAS;IACjB,MAAM,EAAE,QAAQ,GAAG,IAAI,CAAQ;IAC/B,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAQ;IAChC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAQ;IAClC,QAAQ,SAAU;IAClB,OAAO,SAAW;IAClB,cAAc,SAAM;IACpB,GAAG,SAAM;IACT,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAoC;IAC9D,OAAO,CAAC,OAAO,CAAiE;gBAEpE,KAAK,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;IAK3C,IAAI,GAAG,IAAI,MAAM,CAEhB;IAED,oEAAoE;IACpE,IAAI,GAAG,CAAC,KAAK,EAAE,MAAM,EAmBpB;IAED,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI;IASxD,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI;IAI3D,iEAAiE;IACjE,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAKvB,qCAAqC;IACrC,MAAM,CAAC,MAAM,EAAE,sBAAsB,GAAG,IAAI;IAgB5C,OAAO,CAAC,QAAQ;CAKjB"}
|
package/dist/image.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Image` over the bridge's `probeImage`: what a page's `new Image()` gives
|
|
3
|
+
* code that loads a picture to learn its size without drawing it (React's
|
|
4
|
+
* `AsyncImage`, which shows its placeholder meanwhile). Setting `src` starts
|
|
5
|
+
* the probe; `onload` (or a `load` listener) comes with `naturalWidth` and
|
|
6
|
+
* `naturalHeight`, `onerror` when the image could not be loaded or read.
|
|
7
|
+
* Swift fetches the bytes through its image loader, so the `image` element
|
|
8
|
+
* that follows draws from the cache. Nothing else of an `HTMLImageElement`
|
|
9
|
+
* is here: no drawing, no DOM.
|
|
10
|
+
*/
|
|
11
|
+
let bridge = null;
|
|
12
|
+
let nextId = 1;
|
|
13
|
+
const pending = new Map();
|
|
14
|
+
/** Binds `Image` to `native`'s `probeImage`; returns the globals to install. */
|
|
15
|
+
export function createImageGlobals(native) {
|
|
16
|
+
bridge = native;
|
|
17
|
+
nextId = 1;
|
|
18
|
+
pending.clear();
|
|
19
|
+
return { Image: SimpleImage, __sbImageProbed: receiveImageProbe };
|
|
20
|
+
}
|
|
21
|
+
/** The bridge's `__sbImageProbed(id, result)`. */
|
|
22
|
+
export function receiveImageProbe(id, result) {
|
|
23
|
+
const image = pending.get(Number(id));
|
|
24
|
+
if (!image)
|
|
25
|
+
return;
|
|
26
|
+
pending.delete(Number(id));
|
|
27
|
+
image.settle(typeof result === 'string' ? JSON.parse(result) : result);
|
|
28
|
+
}
|
|
29
|
+
export class SimpleImage {
|
|
30
|
+
naturalWidth = 0;
|
|
31
|
+
naturalHeight = 0;
|
|
32
|
+
width = 0;
|
|
33
|
+
height = 0;
|
|
34
|
+
/** True once the load settled, either way, as on the web. */
|
|
35
|
+
complete = false;
|
|
36
|
+
onload = null;
|
|
37
|
+
onerror = null;
|
|
38
|
+
crossOrigin = null;
|
|
39
|
+
decoding = 'auto';
|
|
40
|
+
loading = 'eager';
|
|
41
|
+
referrerPolicy = '';
|
|
42
|
+
alt = '';
|
|
43
|
+
source = '';
|
|
44
|
+
probe = null;
|
|
45
|
+
listeners = new Map();
|
|
46
|
+
waiting = [];
|
|
47
|
+
constructor(width, height) {
|
|
48
|
+
if (width !== undefined)
|
|
49
|
+
this.width = Number(width);
|
|
50
|
+
if (height !== undefined)
|
|
51
|
+
this.height = Number(height);
|
|
52
|
+
}
|
|
53
|
+
get src() {
|
|
54
|
+
return this.source;
|
|
55
|
+
}
|
|
56
|
+
/** Starts loading; an earlier load still under way is forgotten. */
|
|
57
|
+
set src(value) {
|
|
58
|
+
const url = String(value);
|
|
59
|
+
this.source = url;
|
|
60
|
+
this.complete = false;
|
|
61
|
+
this.naturalWidth = 0;
|
|
62
|
+
this.naturalHeight = 0;
|
|
63
|
+
if (this.probe !== null) {
|
|
64
|
+
pending.delete(this.probe);
|
|
65
|
+
this.probe = null;
|
|
66
|
+
}
|
|
67
|
+
if (url === '')
|
|
68
|
+
return;
|
|
69
|
+
if (!bridge?.probeImage) {
|
|
70
|
+
this.settle({ error: 'no image loader' });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const id = nextId++;
|
|
74
|
+
this.probe = id;
|
|
75
|
+
pending.set(id, this);
|
|
76
|
+
bridge.probeImage(id, url);
|
|
77
|
+
}
|
|
78
|
+
addEventListener(type, listener) {
|
|
79
|
+
let set = this.listeners.get(type);
|
|
80
|
+
if (!set) {
|
|
81
|
+
set = new Set();
|
|
82
|
+
this.listeners.set(type, set);
|
|
83
|
+
}
|
|
84
|
+
set.add(listener);
|
|
85
|
+
}
|
|
86
|
+
removeEventListener(type, listener) {
|
|
87
|
+
this.listeners.get(type)?.delete(listener);
|
|
88
|
+
}
|
|
89
|
+
/** Resolves once the image loaded, rejects when it could not. */
|
|
90
|
+
decode() {
|
|
91
|
+
if (this.complete)
|
|
92
|
+
return this.naturalWidth > 0 ? Promise.resolve() : Promise.reject(new Error('The image could not be decoded'));
|
|
93
|
+
return new Promise((resolve, reject) => this.waiting.push({ resolve, reject }));
|
|
94
|
+
}
|
|
95
|
+
/** @internal The bridge's answer. */
|
|
96
|
+
settle(result) {
|
|
97
|
+
this.probe = null;
|
|
98
|
+
this.complete = true;
|
|
99
|
+
const loaded = !result.error && Number(result.width) > 0 && Number(result.height) > 0;
|
|
100
|
+
if (loaded) {
|
|
101
|
+
this.naturalWidth = Number(result.width);
|
|
102
|
+
this.naturalHeight = Number(result.height);
|
|
103
|
+
if (this.width === 0)
|
|
104
|
+
this.width = this.naturalWidth;
|
|
105
|
+
if (this.height === 0)
|
|
106
|
+
this.height = this.naturalHeight;
|
|
107
|
+
}
|
|
108
|
+
const waiting = this.waiting;
|
|
109
|
+
this.waiting = [];
|
|
110
|
+
for (const { resolve, reject } of waiting)
|
|
111
|
+
loaded ? resolve() : reject(new Error(result.error ?? 'The image could not be decoded'));
|
|
112
|
+
this.dispatch(loaded ? 'load' : 'error');
|
|
113
|
+
}
|
|
114
|
+
dispatch(type) {
|
|
115
|
+
const event = { type, target: this };
|
|
116
|
+
(type === 'load' ? this.onload : this.onerror)?.(event);
|
|
117
|
+
for (const listener of this.listeners.get(type) ?? [])
|
|
118
|
+
listener(event);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=image.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image.js","sourceRoot":"","sources":["../src/image.ts"],"names":[],"mappings":"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"}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
* `atob`, `btoa`, `fetch` with `Headers`, `Request`, `Response`,
|
|
12
12
|
* `AbortController` and `AbortSignal`, `URL`, `URLSearchParams`, `crypto`
|
|
13
13
|
* (`getRandomValues`, `randomUUID`), `localStorage`, `structuredClone`,
|
|
14
|
-
* `navigator`, `
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* `navigator`, `Image` (image.ts, on a bridge with `probeImage`) with the
|
|
15
|
+
* bridge's `__sbImageProbed`, `Worker` (worker.ts) with the bridge's
|
|
16
|
+
* `__sbWorkerMessage` and `__sbWorkerError`, `__sbOpenURL` (openURL.ts),
|
|
17
|
+
* and the bridge's callbacks `__sbTimerFired` and `__sbFetchResult`. In a
|
|
18
|
+
* worker's own context (its bridge has
|
|
17
19
|
* `workerPostMessage`), `self`, `postMessage`, `addEventListener` and
|
|
18
20
|
* `__sbWorkerReceive` instead of `Worker`. `window` and `document` are not
|
|
19
21
|
* defined: code that checks for them takes its headless path, as it should
|
package/dist/runtime.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,UAAU,CAAC;AAQ3D,KAAK,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAQzC,MAAM,WAAW,cAAc;IAC7B,uDAAuD;IACvD,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,YAAY,EAAE,MAAM,GAAE,SAA8C,EAAE,OAAO,GAAE,cAAmB,GAAG,MAAM,EAAE,CAsLnJ"}
|
package/dist/runtime.js
CHANGED
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
* `atob`, `btoa`, `fetch` with `Headers`, `Request`, `Response`,
|
|
12
12
|
* `AbortController` and `AbortSignal`, `URL`, `URLSearchParams`, `crypto`
|
|
13
13
|
* (`getRandomValues`, `randomUUID`), `localStorage`, `structuredClone`,
|
|
14
|
-
* `navigator`, `
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* `navigator`, `Image` (image.ts, on a bridge with `probeImage`) with the
|
|
15
|
+
* bridge's `__sbImageProbed`, `Worker` (worker.ts) with the bridge's
|
|
16
|
+
* `__sbWorkerMessage` and `__sbWorkerError`, `__sbOpenURL` (openURL.ts),
|
|
17
|
+
* and the bridge's callbacks `__sbTimerFired` and `__sbFetchResult`. In a
|
|
18
|
+
* worker's own context (its bridge has
|
|
17
19
|
* `workerPostMessage`), `self`, `postMessage`, `addEventListener` and
|
|
18
20
|
* `__sbWorkerReceive` instead of `Worker`. `window` and `document` are not
|
|
19
21
|
* defined: code that checks for them takes its headless path, as it should
|
|
@@ -22,6 +24,7 @@
|
|
|
22
24
|
import { nativeBridge } from './bridge';
|
|
23
25
|
import { atobPolyfill, base64Decode, btoaPolyfill, SimpleTextDecoder, SimpleTextEncoder } from './encoding';
|
|
24
26
|
import { createFetch } from './fetch';
|
|
27
|
+
import { createImageGlobals } from './image';
|
|
25
28
|
import { SimpleURL, SimpleURLSearchParams } from './url';
|
|
26
29
|
import { installWorkerScope, NativeWorker } from './worker';
|
|
27
30
|
import { receiveOpenURL } from './openURL';
|
|
@@ -155,6 +158,13 @@ export function installRuntime(native, target = globalThis, options = {}) {
|
|
|
155
158
|
define('__sbFetchResult', fetchGlobals.__sbFetchResult);
|
|
156
159
|
define('URL', SimpleURL);
|
|
157
160
|
define('URLSearchParams', SimpleURLSearchParams);
|
|
161
|
+
// `Image`, for code that loads a picture to learn its size (React's
|
|
162
|
+
// `AsyncImage`): the host reads it through its image loader.
|
|
163
|
+
if (native.probeImage && !native.workerPostMessage) {
|
|
164
|
+
const imageGlobals = createImageGlobals(native);
|
|
165
|
+
define('Image', imageGlobals.Image);
|
|
166
|
+
define('__sbImageProbed', imageGlobals.__sbImageProbed);
|
|
167
|
+
}
|
|
158
168
|
define('crypto', {
|
|
159
169
|
getRandomValues: (array) => {
|
|
160
170
|
const bytes = base64Decode(native.randomBytes(array.byteLength));
|
package/dist/runtime.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swiftbrowser/native",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Run a @swiftbrowser/react app natively: the runtime a JavaScriptCore-hosted app needs (timers, console, fetch and the other web globals over the SwiftBrowser Native bridge), the `run` entry that renders into the native SwiftUI renderer, and the bundler that builds app.js for the Xcode target.",
|
|
6
6
|
"repository": {
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"@babel/core": "^7.29.7",
|
|
62
|
-
"@swiftbrowser/react": "0.17.
|
|
63
|
-
"@swiftbrowser/web": "0.17.
|
|
62
|
+
"@swiftbrowser/react": "0.17.5",
|
|
63
|
+
"@swiftbrowser/web": "0.17.5",
|
|
64
64
|
"esbuild": "^0.28.2",
|
|
65
65
|
"react-refresh": "^0.17.0"
|
|
66
66
|
},
|
package/src/bridge.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* The native side of the bridge, as Swift installs it on `__sbNative`
|
|
3
3
|
* (Native/Sources/SwiftBrowserNative/Host/HostBridge.swift). Every call is
|
|
4
4
|
* synchronous; the ones that answer later do so by calling a global this
|
|
5
|
-
* package defines (`__sbTimerFired`, `__sbFetchResult`, `
|
|
6
|
-
* `__sbOpenURL`).
|
|
5
|
+
* package defines (`__sbTimerFired`, `__sbFetchResult`, `__sbImageProbed`,
|
|
6
|
+
* `__sbWorkerMessage`, `__sbOpenURL`).
|
|
7
7
|
*/
|
|
8
8
|
export interface NativeBridge {
|
|
9
9
|
/** Applies a render pass's ops (a JSON array). Returns an error message, or nothing. */
|
|
@@ -15,6 +15,14 @@ export interface NativeBridge {
|
|
|
15
15
|
clearTimer(id: number): void;
|
|
16
16
|
/** Starts a request; the answer arrives at `__sbFetchResult(id, result)`. */
|
|
17
17
|
fetch(id: number, request: string | NativeFetchRequest): void;
|
|
18
|
+
/**
|
|
19
|
+
* Starts loading the image at `url` for its pixel size (image.ts: the
|
|
20
|
+
* runtime's `Image`), keeping the bytes for the `image` element that
|
|
21
|
+
* follows; the answer arrives at `__sbImageProbed(id, result)`. Hosts
|
|
22
|
+
* before 0.18 lack it, and so does the page's host for the round trip,
|
|
23
|
+
* which has a real `Image`.
|
|
24
|
+
*/
|
|
25
|
+
probeImage?(id: number, url: string): void;
|
|
18
26
|
storageGet(key: string): string | null;
|
|
19
27
|
storageSet(key: string, value: string): void;
|
|
20
28
|
storageRemove(key: string): void;
|
|
@@ -68,6 +76,12 @@ export interface NativeFetchRequest {
|
|
|
68
76
|
body: string | null;
|
|
69
77
|
}
|
|
70
78
|
|
|
79
|
+
export interface NativeImageProbeResult {
|
|
80
|
+
width?: number;
|
|
81
|
+
height?: number;
|
|
82
|
+
error?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
71
85
|
export interface NativeFetchResult {
|
|
72
86
|
status?: number;
|
|
73
87
|
statusText?: string;
|
package/src/image.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Image` over the bridge's `probeImage`: what a page's `new Image()` gives
|
|
3
|
+
* code that loads a picture to learn its size without drawing it (React's
|
|
4
|
+
* `AsyncImage`, which shows its placeholder meanwhile). Setting `src` starts
|
|
5
|
+
* the probe; `onload` (or a `load` listener) comes with `naturalWidth` and
|
|
6
|
+
* `naturalHeight`, `onerror` when the image could not be loaded or read.
|
|
7
|
+
* Swift fetches the bytes through its image loader, so the `image` element
|
|
8
|
+
* that follows draws from the cache. Nothing else of an `HTMLImageElement`
|
|
9
|
+
* is here: no drawing, no DOM.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { NativeBridge, NativeImageProbeResult } from './bridge';
|
|
13
|
+
|
|
14
|
+
interface ImageEvent {
|
|
15
|
+
type: 'load' | 'error';
|
|
16
|
+
target: SimpleImage;
|
|
17
|
+
}
|
|
18
|
+
type Listener = (event: ImageEvent) => void;
|
|
19
|
+
|
|
20
|
+
let bridge: NativeBridge | null = null;
|
|
21
|
+
let nextId = 1;
|
|
22
|
+
const pending = new Map<number, SimpleImage>();
|
|
23
|
+
|
|
24
|
+
/** Binds `Image` to `native`'s `probeImage`; returns the globals to install. */
|
|
25
|
+
export function createImageGlobals(native: NativeBridge): { Image: typeof SimpleImage; __sbImageProbed: typeof receiveImageProbe } {
|
|
26
|
+
bridge = native;
|
|
27
|
+
nextId = 1;
|
|
28
|
+
pending.clear();
|
|
29
|
+
return { Image: SimpleImage, __sbImageProbed: receiveImageProbe };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The bridge's `__sbImageProbed(id, result)`. */
|
|
33
|
+
export function receiveImageProbe(id: number, result: NativeImageProbeResult | string): void {
|
|
34
|
+
const image = pending.get(Number(id));
|
|
35
|
+
if (!image) return;
|
|
36
|
+
pending.delete(Number(id));
|
|
37
|
+
image.settle(typeof result === 'string' ? (JSON.parse(result) as NativeImageProbeResult) : result);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class SimpleImage {
|
|
41
|
+
naturalWidth = 0;
|
|
42
|
+
naturalHeight = 0;
|
|
43
|
+
width = 0;
|
|
44
|
+
height = 0;
|
|
45
|
+
/** True once the load settled, either way, as on the web. */
|
|
46
|
+
complete = false;
|
|
47
|
+
onload: Listener | null = null;
|
|
48
|
+
onerror: Listener | null = null;
|
|
49
|
+
crossOrigin: string | null = null;
|
|
50
|
+
decoding = 'auto';
|
|
51
|
+
loading = 'eager';
|
|
52
|
+
referrerPolicy = '';
|
|
53
|
+
alt = '';
|
|
54
|
+
private source = '';
|
|
55
|
+
private probe: number | null = null;
|
|
56
|
+
private readonly listeners = new Map<string, Set<Listener>>();
|
|
57
|
+
private waiting: { resolve: () => void; reject: (error: Error) => void }[] = [];
|
|
58
|
+
|
|
59
|
+
constructor(width?: number, height?: number) {
|
|
60
|
+
if (width !== undefined) this.width = Number(width);
|
|
61
|
+
if (height !== undefined) this.height = Number(height);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get src(): string {
|
|
65
|
+
return this.source;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Starts loading; an earlier load still under way is forgotten. */
|
|
69
|
+
set src(value: string) {
|
|
70
|
+
const url = String(value);
|
|
71
|
+
this.source = url;
|
|
72
|
+
this.complete = false;
|
|
73
|
+
this.naturalWidth = 0;
|
|
74
|
+
this.naturalHeight = 0;
|
|
75
|
+
if (this.probe !== null) {
|
|
76
|
+
pending.delete(this.probe);
|
|
77
|
+
this.probe = null;
|
|
78
|
+
}
|
|
79
|
+
if (url === '') return;
|
|
80
|
+
if (!bridge?.probeImage) {
|
|
81
|
+
this.settle({ error: 'no image loader' });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const id = nextId++;
|
|
85
|
+
this.probe = id;
|
|
86
|
+
pending.set(id, this);
|
|
87
|
+
bridge.probeImage(id, url);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
addEventListener(type: string, listener: Listener): void {
|
|
91
|
+
let set = this.listeners.get(type);
|
|
92
|
+
if (!set) {
|
|
93
|
+
set = new Set();
|
|
94
|
+
this.listeners.set(type, set);
|
|
95
|
+
}
|
|
96
|
+
set.add(listener);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
removeEventListener(type: string, listener: Listener): void {
|
|
100
|
+
this.listeners.get(type)?.delete(listener);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Resolves once the image loaded, rejects when it could not. */
|
|
104
|
+
decode(): Promise<void> {
|
|
105
|
+
if (this.complete) return this.naturalWidth > 0 ? Promise.resolve() : Promise.reject(new Error('The image could not be decoded'));
|
|
106
|
+
return new Promise((resolve, reject) => this.waiting.push({ resolve, reject }));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** @internal The bridge's answer. */
|
|
110
|
+
settle(result: NativeImageProbeResult): void {
|
|
111
|
+
this.probe = null;
|
|
112
|
+
this.complete = true;
|
|
113
|
+
const loaded = !result.error && Number(result.width) > 0 && Number(result.height) > 0;
|
|
114
|
+
if (loaded) {
|
|
115
|
+
this.naturalWidth = Number(result.width);
|
|
116
|
+
this.naturalHeight = Number(result.height);
|
|
117
|
+
if (this.width === 0) this.width = this.naturalWidth;
|
|
118
|
+
if (this.height === 0) this.height = this.naturalHeight;
|
|
119
|
+
}
|
|
120
|
+
const waiting = this.waiting;
|
|
121
|
+
this.waiting = [];
|
|
122
|
+
for (const { resolve, reject } of waiting) loaded ? resolve() : reject(new Error(result.error ?? 'The image could not be decoded'));
|
|
123
|
+
this.dispatch(loaded ? 'load' : 'error');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private dispatch(type: 'load' | 'error'): void {
|
|
127
|
+
const event: ImageEvent = { type, target: this };
|
|
128
|
+
(type === 'load' ? this.onload : this.onerror)?.(event);
|
|
129
|
+
for (const listener of this.listeners.get(type) ?? []) listener(event);
|
|
130
|
+
}
|
|
131
|
+
}
|
package/src/runtime.ts
CHANGED
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
* `atob`, `btoa`, `fetch` with `Headers`, `Request`, `Response`,
|
|
12
12
|
* `AbortController` and `AbortSignal`, `URL`, `URLSearchParams`, `crypto`
|
|
13
13
|
* (`getRandomValues`, `randomUUID`), `localStorage`, `structuredClone`,
|
|
14
|
-
* `navigator`, `
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* `navigator`, `Image` (image.ts, on a bridge with `probeImage`) with the
|
|
15
|
+
* bridge's `__sbImageProbed`, `Worker` (worker.ts) with the bridge's
|
|
16
|
+
* `__sbWorkerMessage` and `__sbWorkerError`, `__sbOpenURL` (openURL.ts),
|
|
17
|
+
* and the bridge's callbacks `__sbTimerFired` and `__sbFetchResult`. In a
|
|
18
|
+
* worker's own context (its bridge has
|
|
17
19
|
* `workerPostMessage`), `self`, `postMessage`, `addEventListener` and
|
|
18
20
|
* `__sbWorkerReceive` instead of `Worker`. `window` and `document` are not
|
|
19
21
|
* defined: code that checks for them takes its headless path, as it should
|
|
@@ -23,6 +25,7 @@
|
|
|
23
25
|
import { nativeBridge, type NativeBridge } from './bridge';
|
|
24
26
|
import { atobPolyfill, base64Decode, btoaPolyfill, SimpleTextDecoder, SimpleTextEncoder } from './encoding';
|
|
25
27
|
import { createFetch } from './fetch';
|
|
28
|
+
import { createImageGlobals } from './image';
|
|
26
29
|
import { SimpleURL, SimpleURLSearchParams } from './url';
|
|
27
30
|
import { installWorkerScope, NativeWorker } from './worker';
|
|
28
31
|
import { receiveOpenURL } from './openURL';
|
|
@@ -167,6 +170,14 @@ export function installRuntime(native: NativeBridge, target: AnyGlobal = globalT
|
|
|
167
170
|
define('URL', SimpleURL);
|
|
168
171
|
define('URLSearchParams', SimpleURLSearchParams);
|
|
169
172
|
|
|
173
|
+
// `Image`, for code that loads a picture to learn its size (React's
|
|
174
|
+
// `AsyncImage`): the host reads it through its image loader.
|
|
175
|
+
if (native.probeImage && !native.workerPostMessage) {
|
|
176
|
+
const imageGlobals = createImageGlobals(native);
|
|
177
|
+
define('Image', imageGlobals.Image);
|
|
178
|
+
define('__sbImageProbed', imageGlobals.__sbImageProbed);
|
|
179
|
+
}
|
|
180
|
+
|
|
170
181
|
define('crypto', {
|
|
171
182
|
getRandomValues: <T extends ArrayBufferView>(array: T): T => {
|
|
172
183
|
const bytes = base64Decode(native.randomBytes(array.byteLength));
|
|
@@ -22,6 +22,7 @@ import FoundationBridge // the shim's Foundation stand-ins (ShimAliases.swift)
|
|
|
22
22
|
/// | `now()` | milliseconds, monotonic |
|
|
23
23
|
/// | `setTimer(id, delayMs, repeats)`, `clearTimer(id)` | timers on the main run loop; fire `__sbTimerFired(id)` |
|
|
24
24
|
/// | `fetch(id, requestJSON)` | `URLSession`; answers `__sbFetchResult(id, resultJSON)` |
|
|
25
|
+
/// | `probeImage(id, url)` | the image's pixel size through the image loader (ImageLoader.swift), which keeps the bytes for the `image` that follows; answers `__sbImageProbed(id, {width, height} or {error})`. The runtime's `Image` (a page's `new Image()`). Apple platforms only |
|
|
25
26
|
/// | `storageGet/Set/Remove/Keys/Clear` | `localStorage` over `UserDefaults` |
|
|
26
27
|
/// | `randomBytes(count)` | base64 of random bytes (`crypto.getRandomValues`) |
|
|
27
28
|
/// | `openURL(url)` | the system opener |
|
|
@@ -126,6 +127,22 @@ final class HostBridge {
|
|
|
126
127
|
return nil
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
#if !canImport(FoundationBridge)
|
|
131
|
+
host.install("probeImage") { [weak self] arguments in
|
|
132
|
+
guard let self, let id = arguments[safe: 0]?.int, let string = arguments[safe: 1]?.string else { return nil }
|
|
133
|
+
Task { @MainActor [weak self] in
|
|
134
|
+
let result: JSONValue
|
|
135
|
+
if let url = URL(string: string), let size = await ImageLoader.shared.probe(url) {
|
|
136
|
+
result = .object(["width": .number(Double(size.width)), "height": .number(Double(size.height))])
|
|
137
|
+
} else {
|
|
138
|
+
result = .object(["error": .string("the image could not be loaded")])
|
|
139
|
+
}
|
|
140
|
+
self?.callIn("__sbImageProbed", [.number(Double(id)), result])
|
|
141
|
+
}
|
|
142
|
+
return nil
|
|
143
|
+
}
|
|
144
|
+
#endif
|
|
145
|
+
|
|
129
146
|
host.install("storageGet") { arguments in
|
|
130
147
|
guard let key = arguments.first?.string else { return .null }
|
|
131
148
|
return UserDefaults.standard.string(forKey: HostBridge.storagePrefix + key).map(JSONValue.string) ?? .null
|
|
@@ -21,5 +21,7 @@ public typealias URLSessionConfiguration = FoundationBridge.URLSessionConfigurat
|
|
|
21
21
|
public typealias URLRequest = FoundationBridge.URLRequest
|
|
22
22
|
public typealias URLResponse = FoundationBridge.URLResponse
|
|
23
23
|
public typealias HTTPURLResponse = FoundationBridge.HTTPURLResponse
|
|
24
|
+
public typealias URLCache = FoundationBridge.URLCache
|
|
25
|
+
public typealias CachedURLResponse = FoundationBridge.CachedURLResponse
|
|
24
26
|
public typealias DispatchQueue = FoundationBridge.DispatchQueue
|
|
25
27
|
#endif
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
// Apple platforms only: against the shim (the round trip, docs/native.md)
|
|
2
|
+
// the page loads and draws the images itself, and the shim's URL cache and
|
|
3
|
+
// session are stand-ins.
|
|
4
|
+
#if canImport(SwiftUI) && !canImport(FoundationBridge)
|
|
5
|
+
import CoreGraphics
|
|
6
|
+
import Foundation
|
|
7
|
+
import ImageIO
|
|
8
|
+
#if canImport(SwiftDraw)
|
|
9
|
+
import SwiftDraw
|
|
10
|
+
#endif
|
|
11
|
+
|
|
12
|
+
/// How the image loader caches (`ImageLoader`, below): set once at launch,
|
|
13
|
+
/// before the first image loads, with `ImageCache.configure(_:)`.
|
|
14
|
+
///
|
|
15
|
+
/// ```swift
|
|
16
|
+
/// var options = ImageCacheOptions()
|
|
17
|
+
/// options.diskCapacity = 512 << 20
|
|
18
|
+
/// ImageCache.configure(options)
|
|
19
|
+
/// ```
|
|
20
|
+
public struct ImageCacheOptions: Sendable {
|
|
21
|
+
/// The bytes kept in memory (32 MB) and on disk (256 MB), for as long as the server's cache headers allow.
|
|
22
|
+
public var memoryCapacity: Int = 32 << 20
|
|
23
|
+
public var diskCapacity: Int = 256 << 20
|
|
24
|
+
/// Where the bytes live on disk; nil for `Caches/SwiftBrowserNative/Images`.
|
|
25
|
+
public var directory: URL? = nil
|
|
26
|
+
/// The decoded images kept in memory, in bytes; nil for an eighth of the device's memory, 256 MB at most.
|
|
27
|
+
public var decodedCapacity: Int? = nil
|
|
28
|
+
/// The configuration the images' session is made with (`.default`: the
|
|
29
|
+
/// app's cookies); the loader sets its cache on it. An app whose images
|
|
30
|
+
/// need a header (a bearer token) adds it here.
|
|
31
|
+
public var sessionConfiguration: @Sendable () -> URLSessionConfiguration = { .default }
|
|
32
|
+
|
|
33
|
+
public init() {}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// The caches of the remote images a native app draws (ImageLoader.swift).
|
|
37
|
+
public enum ImageCache {
|
|
38
|
+
/// Sets the caches' sizes and the images' session. Call it at launch,
|
|
39
|
+
/// before the first image loads: it returns false, and changes nothing,
|
|
40
|
+
/// once a session exists.
|
|
41
|
+
@discardableResult
|
|
42
|
+
public static func configure(_ options: ImageCacheOptions) -> Bool {
|
|
43
|
+
ImageLoader.settings.configure(options)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// The options in effect.
|
|
47
|
+
public static var options: ImageCacheOptions { ImageLoader.settings.current }
|
|
48
|
+
|
|
49
|
+
/// Drops every cached byte and decode (a sign-out: the next account's images are its own).
|
|
50
|
+
public static func clear() async {
|
|
51
|
+
await ImageLoader.shared.clear()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// The remote images a native app draws (RemoteImage.swift), cached twice
|
|
56
|
+
/// over, as SwiftUI's `AsyncImage` does not: the bytes in a URL cache of the
|
|
57
|
+
/// loader's own (32 MB in memory, 256 MB on disk, kept across launches, for
|
|
58
|
+
/// as long as the server's cache headers allow), and the decoded images in
|
|
59
|
+
/// memory (up to an eighth of the device's memory, 256 MB at most; the
|
|
60
|
+
/// system evicts them under pressure), each decoded no larger than the box
|
|
61
|
+
/// it is drawn in, through ImageIO's thumbnail path, so a photo drawn as a
|
|
62
|
+
/// 40 pt avatar costs a few kilobytes rather than the tens of megabytes of
|
|
63
|
+
/// its full size. A row that scrolls back on screen, or a screen popped
|
|
64
|
+
/// back to, draws its images at once. `ImageCacheOptions` sizes the caches.
|
|
65
|
+
///
|
|
66
|
+
/// A request names the URL, the pixel size the longer side may have (nil:
|
|
67
|
+
/// the image's own size) and the scale the result is drawn at. The cache
|
|
68
|
+
/// keeps the largest decode of each image at each scale; a decode at the
|
|
69
|
+
/// image's own size covers every request for it.
|
|
70
|
+
///
|
|
71
|
+
/// A `probe` reads an image's size from its header for the app (the
|
|
72
|
+
/// bridge's `probeImage`, HostBridge.swift: a page's `new Image()`), keeping
|
|
73
|
+
/// the bytes for the load that follows.
|
|
74
|
+
actor ImageLoader {
|
|
75
|
+
static let shared = ImageLoader()
|
|
76
|
+
static let settings = ImageLoaderSettings()
|
|
77
|
+
|
|
78
|
+
/// The session every image (and SVG) loads through: the app's cookies,
|
|
79
|
+
/// and the loader's own URL cache for the bytes.
|
|
80
|
+
static var session: URLSession { settings.session() }
|
|
81
|
+
|
|
82
|
+
/// The pixel sizes a resizable image is decoded at (its longer side):
|
|
83
|
+
/// a box that grows a little does not decode again, and the decodes of
|
|
84
|
+
/// one image at several sizes stay few.
|
|
85
|
+
static let sizeLadder = [64, 128, 256, 512, 1024, 2048, 4096]
|
|
86
|
+
|
|
87
|
+
/// The size to decode an image for a box of `box` points at `scale`, on
|
|
88
|
+
/// the ladder: enough pixels for the image to cover the box (knowing the
|
|
89
|
+
/// image's own size, `source`, the exact figure; before, the box's
|
|
90
|
+
/// longer side), or nil for the image's own size when it needs more than
|
|
91
|
+
/// the ladder's top. A box with no size yet (before its first layout) is
|
|
92
|
+
/// nil too: nothing to size for.
|
|
93
|
+
static func pixelSize(for box: CGSize, scale: CGFloat, source: CGSize?) -> Int? {
|
|
94
|
+
guard box.width > 0 || box.height > 0 else { return nil }
|
|
95
|
+
var needed = max(box.width, box.height) * scale
|
|
96
|
+
if let source, source.width > 0, source.height > 0 {
|
|
97
|
+
let cover = max(box.width / source.width, box.height / source.height)
|
|
98
|
+
needed = max(source.width, source.height) * cover * scale
|
|
99
|
+
}
|
|
100
|
+
return sizeLadder.first { CGFloat($0) >= needed }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private nonisolated let store: DecodedImageStore
|
|
104
|
+
private var inFlight: [String: Task<ImageLoad, Never>] = [:]
|
|
105
|
+
/// The bytes a probe fetched, for the load that follows (a response the
|
|
106
|
+
/// server does not let the URL cache keep would be fetched twice).
|
|
107
|
+
private var probed: [URL: Data] = [:]
|
|
108
|
+
private var probedOrder: [URL] = []
|
|
109
|
+
private static let probedLimit = 32
|
|
110
|
+
|
|
111
|
+
init(decodedCapacity: Int? = nil) {
|
|
112
|
+
store = DecodedImageStore(capacity: decodedCapacity ?? Self.settings.current.decodedCapacity)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// The decode in memory that serves `request`, if any (any task's turn: the store is thread-safe).
|
|
116
|
+
nonisolated func cached(_ request: ImageRequest) -> DecodedImage? {
|
|
117
|
+
guard let decoded = store.cache.object(forKey: request.cacheKey as NSString), decoded.covers(request.maxPixelSize) else { return nil }
|
|
118
|
+
return decoded
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// The image for `request`, decoded (and fetched) at most once at a time
|
|
122
|
+
/// per URL and scale, however many views ask.
|
|
123
|
+
func load(_ request: ImageRequest) async -> ImageLoad {
|
|
124
|
+
if let decoded = cached(request) { return .image(decoded) }
|
|
125
|
+
let key = request.cacheKey
|
|
126
|
+
if let running = inFlight[key] {
|
|
127
|
+
let result = await running.value
|
|
128
|
+
if case .image(let decoded) = result, decoded.covers(request.maxPixelSize) { return result }
|
|
129
|
+
// The running decode was for a smaller box: decode again, bigger.
|
|
130
|
+
if inFlight[key] == running { inFlight[key] = nil }
|
|
131
|
+
return await load(request)
|
|
132
|
+
}
|
|
133
|
+
let known = takeProbed(request.url)
|
|
134
|
+
let task = Task<ImageLoad, Never> { await Self.fetchAndDecode(request, data: known) }
|
|
135
|
+
inFlight[key] = task
|
|
136
|
+
let result = await task.value
|
|
137
|
+
if inFlight[key] == task { inFlight[key] = nil }
|
|
138
|
+
if case .image(let decoded) = result { store.cache.setObject(decoded, forKey: key as NSString, cost: decoded.cost) }
|
|
139
|
+
return result
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private static func fetchAndDecode(_ request: ImageRequest, data known: Data?) async -> ImageLoad {
|
|
143
|
+
let data: Data
|
|
144
|
+
if let known {
|
|
145
|
+
data = known
|
|
146
|
+
} else {
|
|
147
|
+
guard let fetched = await fetch(request.url) else { return .failed }
|
|
148
|
+
data = fetched
|
|
149
|
+
}
|
|
150
|
+
// Decoding is work for a thread of its own, not the loader's turn.
|
|
151
|
+
let task = Task.detached(priority: .userInitiated) { decodeImage(data, url: request.url, maxPixelSize: request.maxPixelSize, scale: request.scale) }
|
|
152
|
+
guard let decoded = await task.value else { return .undecodable }
|
|
153
|
+
return .image(decoded)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// The bytes at `url` through the session, or nil when they cannot be had.
|
|
157
|
+
private static func fetch(_ url: URL) async -> Data? {
|
|
158
|
+
guard let fetched = try? await session.data(from: url) else { return nil }
|
|
159
|
+
if let http = fetched.1 as? HTTPURLResponse, !(200..<300).contains(http.statusCode) { return nil }
|
|
160
|
+
return fetched.0
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// The pixel size of the image at `url`, as oriented, read from its
|
|
164
|
+
/// header without a decode (an SVG's from its markup), or nil when it
|
|
165
|
+
/// cannot be fetched or read. The bytes stay for the load that follows.
|
|
166
|
+
func probe(_ url: URL) async -> CGSize? {
|
|
167
|
+
guard let data = await Self.fetch(url) else { return nil }
|
|
168
|
+
guard let size = imageSize(of: data) else { return nil }
|
|
169
|
+
keepProbed(data, for: url)
|
|
170
|
+
return size
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private func keepProbed(_ data: Data, for url: URL) {
|
|
174
|
+
if probed[url] == nil { probedOrder.append(url) }
|
|
175
|
+
probed[url] = data
|
|
176
|
+
while probedOrder.count > Self.probedLimit {
|
|
177
|
+
probed[probedOrder.removeFirst()] = nil
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private func takeProbed(_ url: URL) -> Data? {
|
|
182
|
+
guard let data = probed.removeValue(forKey: url) else { return nil }
|
|
183
|
+
probedOrder.removeAll { $0 == url }
|
|
184
|
+
return data
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/// Drops every cached byte and decode.
|
|
188
|
+
func clear() {
|
|
189
|
+
store.cache.removeAllObjects()
|
|
190
|
+
probed.removeAll()
|
|
191
|
+
probedOrder.removeAll()
|
|
192
|
+
Self.session.configuration.urlCache?.removeAllCachedResponses()
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/// The loader's options and the session made from them, which fixes them.
|
|
197
|
+
final class ImageLoaderSettings: @unchecked Sendable {
|
|
198
|
+
private let lock = NSLock()
|
|
199
|
+
private var options = ImageCacheOptions()
|
|
200
|
+
private var made: URLSession?
|
|
201
|
+
|
|
202
|
+
/// Takes `options` unless a session exists already; returns whether it did.
|
|
203
|
+
func configure(_ options: ImageCacheOptions) -> Bool {
|
|
204
|
+
lock.lock()
|
|
205
|
+
defer { lock.unlock() }
|
|
206
|
+
guard made == nil else { return false }
|
|
207
|
+
self.options = options
|
|
208
|
+
return true
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
var current: ImageCacheOptions {
|
|
212
|
+
lock.lock()
|
|
213
|
+
defer { lock.unlock() }
|
|
214
|
+
return options
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/// The images' session, made once from the options.
|
|
218
|
+
func session() -> URLSession {
|
|
219
|
+
lock.lock()
|
|
220
|
+
defer { lock.unlock() }
|
|
221
|
+
if let made { return made }
|
|
222
|
+
let configuration = options.sessionConfiguration()
|
|
223
|
+
let directory = options.directory ?? FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?
|
|
224
|
+
.appendingPathComponent("SwiftBrowserNative", isDirectory: true)
|
|
225
|
+
.appendingPathComponent("Images", isDirectory: true)
|
|
226
|
+
configuration.urlCache = URLCache(memoryCapacity: options.memoryCapacity, diskCapacity: options.diskCapacity, directory: directory)
|
|
227
|
+
configuration.requestCachePolicy = .useProtocolCachePolicy
|
|
228
|
+
let session = URLSession(configuration: configuration)
|
|
229
|
+
made = session
|
|
230
|
+
return session
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/// The decoded images in memory (`NSCache`: thread-safe, evicted under memory pressure), held where every task reaches them.
|
|
235
|
+
private final class DecodedImageStore: @unchecked Sendable {
|
|
236
|
+
let cache = NSCache<NSString, DecodedImage>()
|
|
237
|
+
|
|
238
|
+
init(capacity: Int?) {
|
|
239
|
+
let memory = ProcessInfo.processInfo.physicalMemory
|
|
240
|
+
cache.totalCostLimit = capacity ?? Int(min(memory / 8, 256 << 20))
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/// What a remote image is, once drawn: the decode, or the bytes ImageIO could not read (an SVG, maybe), or nothing to show.
|
|
245
|
+
enum ImageLoad: Sendable {
|
|
246
|
+
case image(DecodedImage)
|
|
247
|
+
case undecodable
|
|
248
|
+
case failed
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
struct ImageRequest: Equatable, Hashable, Sendable {
|
|
252
|
+
let url: URL
|
|
253
|
+
/// The longer side's pixels at most; nil for the image's own size.
|
|
254
|
+
let maxPixelSize: Int?
|
|
255
|
+
/// The scale the decode is drawn at: the display's for a resizable image, 1 for one drawn at its own size.
|
|
256
|
+
let scale: CGFloat
|
|
257
|
+
|
|
258
|
+
var cacheKey: String { "\(url.absoluteString)@\(scale)" }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/// A decoded image (immutable, so shared freely) with what it was decoded from and for.
|
|
262
|
+
final class DecodedImage: @unchecked Sendable {
|
|
263
|
+
let url: URL
|
|
264
|
+
let cgImage: CGImage
|
|
265
|
+
let scale: CGFloat
|
|
266
|
+
/// The limit it was decoded under; nil for the image's own size, which covers every request.
|
|
267
|
+
let maxPixelSize: Int?
|
|
268
|
+
/// The image's own size in pixels, as oriented.
|
|
269
|
+
let sourceSize: CGSize
|
|
270
|
+
|
|
271
|
+
init(url: URL, cgImage: CGImage, scale: CGFloat, maxPixelSize: Int?, sourceSize: CGSize) {
|
|
272
|
+
self.url = url
|
|
273
|
+
self.cgImage = cgImage
|
|
274
|
+
self.scale = scale
|
|
275
|
+
self.maxPixelSize = maxPixelSize
|
|
276
|
+
self.sourceSize = sourceSize
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/// Whether a decode this size serves a request for `needed` pixels (nil: the image's own size).
|
|
280
|
+
func covers(_ needed: Int?) -> Bool {
|
|
281
|
+
guard let maxPixelSize else { return true }
|
|
282
|
+
guard let needed else { return false }
|
|
283
|
+
return maxPixelSize >= needed
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/// The memory the decode holds.
|
|
287
|
+
var cost: Int { cgImage.bytesPerRow * cgImage.height }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/// `data` decoded through ImageIO with its longer side at most `maxPixelSize`
|
|
291
|
+
/// pixels (nil, or a limit the image is within: at its own size, which the
|
|
292
|
+
/// result's `maxPixelSize` then reports as nil), oriented as its metadata
|
|
293
|
+
/// says; nil when ImageIO does not read it.
|
|
294
|
+
func decodeImage(_ data: Data, url: URL, maxPixelSize: Int?, scale: CGFloat) -> DecodedImage? {
|
|
295
|
+
guard let source = CGImageSourceCreateWithData(data as CFData, nil), CGImageSourceGetCount(source) > 0 else { return nil }
|
|
296
|
+
let (width, height) = pixelSize(of: source)
|
|
297
|
+
let longest = Int(max(width, height).rounded(.up))
|
|
298
|
+
// The whole image when it is within the limit: the limit is then moot.
|
|
299
|
+
let limit = maxPixelSize.flatMap { longest > 0 && $0 >= longest ? nil : $0 }
|
|
300
|
+
var options: [CFString: Any] = [
|
|
301
|
+
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
|
302
|
+
kCGImageSourceCreateThumbnailWithTransform: true,
|
|
303
|
+
kCGImageSourceShouldCacheImmediately: true,
|
|
304
|
+
]
|
|
305
|
+
options[kCGImageSourceThumbnailMaxPixelSize] = limit ?? max(longest, 1)
|
|
306
|
+
guard let image = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else { return nil }
|
|
307
|
+
return DecodedImage(url: url, cgImage: image, scale: scale, maxPixelSize: limit, sourceSize: CGSize(width: width, height: height))
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/// An image's pixel size as oriented, from its header (EXIF orientations 5 to 8 turn it on its side).
|
|
311
|
+
private func pixelSize(of source: CGImageSource) -> (width: Double, height: Double) {
|
|
312
|
+
let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any] ?? [:]
|
|
313
|
+
var width = (properties[kCGImagePropertyPixelWidth] as? NSNumber)?.doubleValue ?? 0
|
|
314
|
+
var height = (properties[kCGImagePropertyPixelHeight] as? NSNumber)?.doubleValue ?? 0
|
|
315
|
+
if let orientation = (properties[kCGImagePropertyOrientation] as? NSNumber)?.intValue, orientation >= 5 { swap(&width, &height) }
|
|
316
|
+
return (width, height)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/// The size of the image `data` holds, without decoding it: ImageIO's
|
|
320
|
+
/// header, else an SVG's own size (SwiftDraw), else nil.
|
|
321
|
+
func imageSize(of data: Data) -> CGSize? {
|
|
322
|
+
if let source = CGImageSourceCreateWithData(data as CFData, nil), CGImageSourceGetCount(source) > 0 {
|
|
323
|
+
let (width, height) = pixelSize(of: source)
|
|
324
|
+
if width > 0, height > 0 { return CGSize(width: width, height: height) }
|
|
325
|
+
}
|
|
326
|
+
#if canImport(SwiftDraw)
|
|
327
|
+
if let svg = SVG(data: data), svg.size.width > 0, svg.size.height > 0 { return svg.size }
|
|
328
|
+
#endif
|
|
329
|
+
return nil
|
|
330
|
+
}
|
|
331
|
+
#endif
|
|
@@ -8,11 +8,10 @@ import FoundationBridge // `AsyncImage`, against the shim
|
|
|
8
8
|
import SwiftDraw
|
|
9
9
|
#endif
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
///
|
|
13
|
-
///
|
|
14
|
-
///
|
|
15
|
-
/// trip's build of these views has the page, which draws SVG itself.
|
|
11
|
+
#if canImport(FoundationBridge)
|
|
12
|
+
/// An `image` with a URL against the shim (the round trip, docs/native.md):
|
|
13
|
+
/// SwiftUI's `AsyncImage`, which the page loads, draws and caches as a
|
|
14
|
+
/// browser does. On Apple platforms the view below draws it instead.
|
|
16
15
|
struct RemoteImage: View {
|
|
17
16
|
let url: URL?
|
|
18
17
|
let resizable: Bool
|
|
@@ -49,11 +48,109 @@ struct RemoteImage: View {
|
|
|
49
48
|
#endif
|
|
50
49
|
}
|
|
51
50
|
}
|
|
51
|
+
#else
|
|
52
|
+
/// An `image` with a URL, through the package's image loader
|
|
53
|
+
/// (ImageLoader.swift: the bytes and the decodes cached, each decode no
|
|
54
|
+
/// larger than the box it is drawn in), and for an SVG, which the system's
|
|
55
|
+
/// image decoding does not read (a logo found on a website often is one),
|
|
56
|
+
/// SwiftDraw's parser and renderer, drawn as vectors at any size. SwiftDraw
|
|
57
|
+
/// is the package's dependency on Apple platforms only; the round trip's
|
|
58
|
+
/// build of these views has the page, which draws SVG itself.
|
|
59
|
+
///
|
|
60
|
+
/// A resizable image is decoded for the box its layout gives it (at the
|
|
61
|
+
/// display's scale), and again, larger, should the box grow past what the
|
|
62
|
+
/// decode covers (a photo scaled to fit a width learns its height from the
|
|
63
|
+
/// first decode); one drawn at its own size is decoded whole at scale 1, as
|
|
64
|
+
/// `AsyncImage` drew it.
|
|
65
|
+
struct RemoteImage: View {
|
|
66
|
+
let url: URL?
|
|
67
|
+
let resizable: Bool
|
|
68
|
+
/// The image's own size in pixels when the app knows it (a probe told
|
|
69
|
+
/// it, ImageLoader.swift; 0 before): the box of one drawn at its own
|
|
70
|
+
/// size while it loads, and the shape a resizable one is decoded for.
|
|
71
|
+
let width: Double
|
|
72
|
+
let height: Double
|
|
73
|
+
@Environment(\.displayScale) private var displayScale
|
|
74
|
+
@State private var loaded: Loaded?
|
|
75
|
+
/// The box the layout gives the image; zero before the first layout.
|
|
76
|
+
@State private var box: CGSize = .zero
|
|
77
|
+
|
|
78
|
+
private struct Loaded {
|
|
79
|
+
let url: URL
|
|
80
|
+
let result: ImageLoad
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private var decoded: DecodedImage? {
|
|
84
|
+
guard let loaded, loaded.url == url, case .image(let decoded) = loaded.result else { return nil }
|
|
85
|
+
return decoded
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// What to load for the box the image has now: nothing until a resizable image has a box.
|
|
89
|
+
private var request: ImageRequest? {
|
|
90
|
+
guard let url else { return nil }
|
|
91
|
+
if !resizable { return ImageRequest(url: url, maxPixelSize: nil, scale: 1) }
|
|
92
|
+
guard box.width > 0 || box.height > 0 else { return nil }
|
|
93
|
+
let known = width > 0 && height > 0 ? CGSize(width: width, height: height) : nil
|
|
94
|
+
return ImageRequest(url: url, maxPixelSize: ImageLoader.pixelSize(for: box, scale: displayScale, source: decoded?.sourceSize ?? known), scale: displayScale)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
var body: some View {
|
|
98
|
+
content
|
|
99
|
+
.onGeometryChange(for: CGSize.self) { proxy in proxy.size } action: { size in box = size }
|
|
100
|
+
.task(id: request) { await load() }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@ViewBuilder private var content: some View {
|
|
104
|
+
if let decoded {
|
|
105
|
+
let image = Image(decorative: decoded.cgImage, scale: decoded.scale)
|
|
106
|
+
if resizable { image.resizable() } else { image }
|
|
107
|
+
} else if let loaded, loaded.url == url, case .undecodable = loaded.result, let url {
|
|
108
|
+
undecoded(url)
|
|
109
|
+
} else {
|
|
110
|
+
placeholder
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private func load() async {
|
|
115
|
+
guard let request else { return }
|
|
116
|
+
// The decode in memory shows at once, without a placeholder in between.
|
|
117
|
+
if let cached = ImageLoader.shared.cached(request) {
|
|
118
|
+
loaded = Loaded(url: request.url, result: .image(cached))
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
// What is shown stays up while a larger decode of the same image comes.
|
|
122
|
+
if let decoded, decoded.url == request.url, decoded.covers(request.maxPixelSize) { return }
|
|
123
|
+
let result = await ImageLoader.shared.load(request)
|
|
124
|
+
guard !Task.isCancelled else { return }
|
|
125
|
+
loaded = Loaded(url: request.url, result: result)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/// Nothing: at the image's own size when the app knows it and the image
|
|
129
|
+
/// is drawn at that size; else whatever the layout proposes, as a
|
|
130
|
+
/// resizable image takes.
|
|
131
|
+
@ViewBuilder private var placeholder: some View {
|
|
132
|
+
if !resizable, width > 0, height > 0 {
|
|
133
|
+
Color.clear.frame(width: width, height: height)
|
|
134
|
+
} else {
|
|
135
|
+
Color.clear
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/// What ImageIO could not decode: an SVG, drawn by SwiftDraw, else nothing.
|
|
140
|
+
@ViewBuilder private func undecoded(_ url: URL) -> some View {
|
|
141
|
+
#if canImport(SwiftDraw)
|
|
142
|
+
RemoteSVG(url: url, resizable: resizable) { placeholder }
|
|
143
|
+
#else
|
|
144
|
+
placeholder
|
|
145
|
+
#endif
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
#endif
|
|
52
149
|
|
|
53
150
|
#if canImport(SwiftDraw)
|
|
54
|
-
/// The SVG at `url`, loaded through the
|
|
55
|
-
///
|
|
56
|
-
/// when the data is not an SVG.
|
|
151
|
+
/// The SVG at `url`, loaded through the image loader's session (so the
|
|
152
|
+
/// app's cookies apply and the bytes just fetched come from its cache), or
|
|
153
|
+
/// the placeholder while it loads and when the data is not an SVG.
|
|
57
154
|
struct RemoteSVG<Placeholder: View>: View {
|
|
58
155
|
let url: URL
|
|
59
156
|
let resizable: Bool
|
|
@@ -80,7 +177,7 @@ struct LoadedSVG: Equatable {
|
|
|
80
177
|
}
|
|
81
178
|
|
|
82
179
|
/// The SVG at `url`, or nil when it cannot be loaded or is not an SVG.
|
|
83
|
-
func loadSVG(from url: URL, session: URLSession = .
|
|
180
|
+
func loadSVG(from url: URL, session: URLSession = ImageLoader.session) async -> SVG? {
|
|
84
181
|
guard let data = try? await session.data(from: url).0 else { return nil }
|
|
85
182
|
return SVG(data: data)
|
|
86
183
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: package/wasm/SwiftBrowserNative.wasm
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 49333823 bytes
|
|
4
|
+
sha256: f14edd41221a10a6f752f899f5eff663bbdbadd186d7503fad5eabe1e94bf916
|