WKWebView: How to handle BLOB URL
Note that this is a really roundabout hack. Scan all href and replace any blob urls detected with datauri.
EDIT: updated to show it running
function blobToDataURL(blob, callback) {
var a = new FileReader();
a.onload = function(e) {callback(e.target.result);}
a.readAsDataURL(blob);
}
// not sure what elements you are going to intercept:
document.querySelectorAll('a').forEach(async (el)=>{
const url = el.getAttribute('href');
if( url.indexOf('blob:')===0 ) {
let blob = await fetch(url).then(r => r.blob());
blobToDataURL(blob, datauri => el.setAttribute('href',datauri));
}
});
b=new Blob([new Int8Array([1,2,3,4,5,6,7,8,9,10]).buffer]);
test.href=URL.createObjectURL(b);
b=new Blob([new Int8Array([31,32,33,34,35]).buffer]);
test1.href=URL.createObjectURL(b);
b=new Blob([new Int8Array([51,52,53,54]).buffer]);
test2.href=URL.createObjectURL(b);
function blobToDataURL(blob, callback) {
var a = new FileReader();
a.onload = function(e) {callback(e.target.result);}
a.readAsDataURL(blob);
}
document.addEventListener('click', function(event) {
event.preventDefault();
if ( event.target.matches('a[href^="blob:"]') )
(async el=>{
const url = el.href;
const blob = await fetch(url).then(r => r.blob());
blobToDataURL(blob, datauri => el.href=datauri);
})(event.target);
});
// not sure what elements you are going to intercept:
/*document.querySelectorAll('a').forEach(async (el)=>{
const url = el.href;
if( url.indexOf('blob:')===0 ) {
let blob = await fetch(url).then(r => r.blob());
blobToDataURL(blob, datauri => el.href=datauri);
}
});*/
<a id="test">test</a>
<a id="test1">test</a>
<a id="test2">test</a>
How to download a blob URI using AlamoFire
After a few days, I was able to figure out how to download a blob URL without WKDownloadDelegate. The following code builds upon this answer.
A message handler needs to be created to respond to JS messages. I created this in the makeUIView
function
webModel.webView.configuration.userContentController.add(context.coordinator, name: "jsListener")
Inside your WKNavigationDelegate, you need to add this code on a navigation action.
NOTE: Since I use SwiftUI, all of my variables/models are located in the parent class (UIViewRepresentable coordinator).
func webView(_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
if let url = navigationAction.request.url, let scheme = url.scheme?.lowercased() {
if scheme == "blob" {
// Defer to JS handling
parent.webModel.executeBlobDownloadJS(url: url)
decisionHandler(.cancel)
} else {
decisionHandler(.allow)
}
}
}
Here's the JS to request for the blob stored in the browser memory. I added this JS in a wrapper function which called evaluateJavaScript
with the url for cleanliness of my code.
function blobToDataURL(blob, callback) {
var reader = new FileReader()
reader.onload = function(e) {callback(e.target.result.split(",")[1])}
reader.readAsDataURL(blob)
}
async function run() {
const url = "\(url)"
const blob = await fetch(url).then(r => r.blob())
blobToDataURL(blob, datauri => {
const responseObj = {
url: url,
mimeType: blob.type,
size: blob.size,
dataString: datauri
}
window.webkit.messageHandlers.jsListener.postMessage(JSON.stringify(responseObj))
})
}
run()
In addition to the returned JS object, I had to make a struct where I can deserialize the JSON string:
struct BlobComponents: Codable {
let url: String
let mimeType: String
let size: Int64
let dataString: String
}
I then took the messages sent to the WKScriptMessageHandler and interpreted them for saving to files. I used the SwiftUI file mover here, but you can do anything you want with this content.
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard let jsonString = message.body as? String else {
return
}
parent.webModel.blobDownloadWith(jsonString: jsonString)
}
In my web model (needed to import CoreServices):
func blobDownloadWith(jsonString: String) {
guard let jsonData = jsonString.data(using: .utf8) else {
print("Cannot convert blob JSON into data!")
return
}
let decoder = JSONDecoder()
do {
let file = try decoder.decode(BlobComponents.self, from: jsonData)
guard let data = Data(base64Encoded: file.dataString),
let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, file.mimeType as CFString, nil),
let ext = UTTypeCopyPreferredTagWithClass(uti.takeRetainedValue(), kUTTagClassFilenameExtension)
else {
print("Error! \(error)")
return
}
let fileName = file.url.components(separatedBy: "/").last ?? "unknown"
let path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let url = path.appendingPathComponent("blobDownload-\(fileName).\(ext.takeRetainedValue())")
try data.write(to: url)
downloadFileUrl = url
showFileMover = true
} catch {
print("Error! \(error)")
return
}
}
Related Topics
Subtle Cast Warning When Using SQLite.Swift ... Binding? to Any
In Swift,There's No Way to Get the Returned Function's Argument Names
Create an Outlet in Storyboard to an Inherited Property
How to Send Push Notifications Without Using Firebase Console
Opposite of _Conversion in Swift to Assign to a Value of a Different Type
Swift Performseguewithidentifier Shows Black Screen
Why Swift Throws Error When Using Optional Param in Closure Func
Incorrect String to Date Conversion Swift 3.0
Perform Segue from Another Class with Helper Function
Swift 3 - Uibutton Adding Settitle from Plist and Database
What's the Best Way to Iterate Over Results from an API, and Know When It's Finished
Label Disappear When Changing Font Size to 25 in Swift
Uibarbuttonitem Doesn't Work When Created as a Property, But Does When Created in a Function
Why Property Observer Doesn't Work with Classes
Having Tab Bar and Navigationbar in the Same View in Swiftui
Swift: Set Insertion Point in Nstextfield