Skip to content

Instantly share code, notes, and snippets.

@soltrinox
Forked from otmb/ContentView.swift
Created March 5, 2024 05:30
Show Gist options
  • Save soltrinox/22ee5cedbc54dae74df71b73efbe0816 to your computer and use it in GitHub Desktop.
Save soltrinox/22ee5cedbc54dae74df71b73efbe0816 to your computer and use it in GitHub Desktop.
WKWebView can read a wasm from a localfile with XMLHttpRequest, but returns an error with fetch

Abstract

When I tried to use wasm from Swift, I ran into a strange phenomenon.
I get an error when fetching wasm of localfile in WKWebView.
Unhandled Promise Rejection: TypeError: Unexpected response MIME type. Expected 'application/wasm'
It's puzzling because you can read wasm with XMLHttpRequest.

Solution

Overwrite fetch with XMLHttpRequest.

async function fetch(filename) {
    const promise = new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('GET', filename);
        xhr.responseType = 'arraybuffer';
        xhr.addEventListener('load', (e) => resolve(xhr.response));
        xhr.send();
    });

    const response = new Response(await promise.then(), {headers: new Headers([['Content-Type', 'application/wasm']])});

    return response;
}

Wasm Reference source

https://wasmbyexample.dev/examples/hello-world/hello-world.assemblyscript.en-us.html

// install
$ brew install wasmer
$ npm install -g assemblyscript

// edit
$ vi hello-world.ts

export function add(a: i32, b: i32): i32 {
  return a + b;
}

// compile
$ asc hello-world.ts -b hello-world.wasm

// execution
$ wasmer hello-world.wasm -i add 1 2
3

Environment

  • Mac 12.2.1(21D62)
  • Xcode Version 13.2.1 (13C100)

Good News

There is good news. This way Pyodide worked on iOS.
Thank.

import SwiftUI
import WebKit
enum WebViewError: Error {
case contentConversion(String)
case emptyFileName
case inivalidFilePath
var message: String {
switch self {
case let .contentConversion(message):
return "There was an error converting the file path to an HTML String. Error \(message)"
case .emptyFileName:
return "The file name was empty."
case .inivalidFilePath:
return "The file path is invalid."
}
}
}
struct WebView: UIViewRepresentable {
let htmlFileName: String
let onError: (WebViewError) -> Void
func makeUIView(context: Context) -> some UIView {
// Allow access to local files.
let config = WKWebViewConfiguration()
config.preferences.setValue(true, forKey: "allowFileAccessFromFileURLs")
let webView = WKWebView(frame: .zero, configuration: config)
webView.load(htmlFileName, onError: onError)
return webView
}
func updateUIView(_ uiView: UIViewType, context: Context) {}
}
extension WKWebView {
func load(_ htmlFileName: String, onError: (WebViewError) -> Void){
guard !htmlFileName.isEmpty else {
return onError(.emptyFileName)
}
guard let filePath = Bundle.main.path(forResource: htmlFileName, ofType: "html") else {
return onError(.inivalidFilePath)
}
do {
let htmlString = try String(contentsOfFile: filePath, encoding: .utf8)
loadHTMLString(htmlString, baseURL: URL(fileURLWithPath: filePath))
} catch let error {
onError(.contentConversion(error.localizedDescription))
}
}
}
struct ContentView: View {
var body: some View {
WebView(htmlFileName: "hello", onError: { error in
print(error.message)
})
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
async function fetch(filename) {
const promise = new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', filename);
xhr.responseType = 'arraybuffer';
xhr.addEventListener('load', (e) => resolve(xhr.response));
xhr.send();
});
const response = new Response(await promise.then(), {headers: new Headers([['Content-Type', 'application/wasm']])});
return response;
}
export const wasmBrowserInstantiate = async (wasmModuleUrl, importObject) => {
let response = undefined;
if (!importObject) {
importObject = {
env: {
abort: () => console.log("Abort!")
}
};
}
if (WebAssembly.instantiateStreaming) {
// Fetch the module, and instantiate it as it is downloading
response = await WebAssembly.instantiateStreaming(
fetch(wasmModuleUrl),
importObject
);
} else {
// Fallback to using fetch to download the entire module
// And then instantiate the module
const fetchAndInstantiateTask = async () => {
const wasmArrayBuffer = await fetch(wasmModuleUrl).then(response =>
response.arrayBuffer());
return WebAssembly.instantiate(wasmArrayBuffer, importObject);
};
response = await fetchAndInstantiateTask();
}
return response;
};
const runWasmAdd = async () => {
// Instantiate our wasm module
const wasmModule = await wasmBrowserInstantiate("./hello-world.wasm");
// Call the Add function export from wasm, save the result
const addResult = wasmModule.instance.exports.add(24, 24);
// Set the result onto the body
document.body.textContent = `Hello World! addResult: ${addResult}`;
};
runWasmAdd();
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hello WKWebView</title>
<script type="module" src="./hello-world.js"></script>
</head>
<body>
<div id="textfield">Waiting ..</div>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment