SwiftUI display gif image - swift

The way to display animated gif image in swiftUI
because of Image
Image("fall-leaves")
does not support gifs
answer below

Easiest and fastest way to display gif image in swiftUI - is to use Preview / QuickLook (QL) / QLPreviewView
Quartz available only in macOS 10.4+ https://developer.apple.com/documentation/quartz
import SwiftUI
import Quartz
struct QLImage: NSViewRepresentable {
var url: URL
func makeNSView(context: NSViewRepresentableContext<QLImage>) -> QLPreviewView {
let preview = QLPreviewView(frame: .zero, style: .normal)
preview?.autostarts = true
preview?.previewItem = url as QLPreviewItem
return preview ?? QLPreviewView()
}
func updateNSView(_ nsView: QLPreviewView, context: NSViewRepresentableContext<QLImage>) {
nsView.previewItem = url as QLPreviewItem
}
typealias NSViewType = QLPreviewView
}

I was having a hard time using this in my macOS app because I try to load my gif file from the local assets rather than a URL. After looking around for answers, the best answer seems to be putting the gif file into the project folder rather than into the Assets.xcassets and then load the resource into the URL. Somehow Bundle.main.url(forResource: myFile, withExtension: "gif") will return nil if the file isn't in the main project folder. I checked the ProjectName -> Target -> Build Phases -> Copy Bundle Resources and the gif file is there automatically when I add to the project. The weird thing is somehow the gif file is not available if it's in the Assets.xcassets.
I updated the QLImage struct from #Andrew to load the gif file by name (make sure to add the gif directly to your project as mentioned above). Here's the updated code below. You need to also provide a sample gif image called preview-gif or call it whatever you want and match the name in the preview section.
import SwiftUI
import Quartz
struct QLImage: NSViewRepresentable {
private let name: String
init(_ name: String) {
self.name = name
}
func makeNSView(context: NSViewRepresentableContext<QLImage>) -> QLPreviewView {
guard let url = Bundle.main.url(forResource: name, withExtension: "gif")
else {
let _ = print("Cannot get image \(name)")
return QLPreviewView()
}
let preview = QLPreviewView(frame: .zero, style: .normal)
preview?.autostarts = true
preview?.previewItem = url as QLPreviewItem
return preview ?? QLPreviewView()
}
func updateNSView(_ nsView: QLPreviewView, context: NSViewRepresentableContext<QLImage>) {
guard let url = Bundle.main.url(forResource: name, withExtension: "gif")
else {
let _ = print("Cannot get image \(name)")
return
}
nsView.previewItem = url as QLPreviewItem
}
typealias NSViewType = QLPreviewView
}
struct QLImage_Previews: PreviewProvider {
static var previews: some View {
QLImage("preview-gif")
}
}
This will return a view, but you need to specify the frame size or you can't see anything. Use it like this it a view:
QLImage("myGif").frame(width: 500, height: 300, alignment: .center)
Set whatever frame size is appropriate. All the other view-related properties are also available with this.
Hope this helps somebody else who is trying to display GIF images from local resources on macOS. Apple should really make it easier to display animated GIF since it's being used everywhere these days.

Related

Insert Authorization Header field with kingfisher lib in SwiftUI

In swift I'm sending authorization token in header with KF like this:
imageView.kf.setImage(with: URL(string: imageUrl),
placeholder: UIImage(named: placeholderImageName ?? ""),
options: [.requestModifier(MediaProxyHelperModifier.shared.modifier)]) //<- a singleton class that used to create a token
but I unable to find out the solution to pass the same in SwiftUI, Although KFImage is available for SwiftUI but with very limited option.
Or do we have any other option to achieve the same?
For a hack, I'm using it like this
extension Image {
func getImage(url: String?, placeholder: String?) -> Self {
let imageView = UIImageView()
imageView.setImage(with: url, placeholderImageName: placeholder)
if let image = imageView.image {
return Image(uiImage: image)
} else {
return Image(placeholder ?? "")
}
}
}
but this solution is not so perfect, I mean very first time images are not loading but when refreshing the UI or revisit the same it started working.
IMAGE 1: first visited
IMAGE 2: tab changed or refreshed view

How to change a folder of Reference Images?

I'm trying to code with my iPad and the Swift Playgrounds 4.0. I tried to do Image Tracking with SwiftUI example 1 or example 2. It's possible to create a folder in the iPad App, but you can't put an Image in here... So the following code doesn't work:
func makeUIView(context: Context) -> ARView {
guard let referenceImages = ARReferenceImage.referenceImages(
inGroupNamed: "AR Resources",
bundle: nil)
else {
fatalError("Missing expected asset catalog resources.")
}
}
Here an Image from the Swift Playgrounds App
Is it possible to use reference images from the root / main like you do with the .usdz models?
if let usdzModel = try? Entity.load(named: "drummer") {
anchor.addChild(usdzModel)
}
Here is the complete Code:
import ARKit
import SwiftUI
import RealityKit
struct RealityKitView: UIViewRepresentable {
func makeUIView(context: Context) -> ARView {
let view = ARView()
// Start AR session
let session = view.session
let config = ARWorldTrackingConfiguration()
config.planeDetection = [.horizontal]
session.run(config)
// Add coaching overlay
let coachingOverlay = ARCoachingOverlayView()
coachingOverlay.autoresizingMask = [.flexibleWidth, .flexibleHeight]
coachingOverlay.session = session
coachingOverlay.goal = .horizontalPlane
view.addSubview(coachingOverlay)
// Set debug options
#if DEBUG
view.debugOptions = [.showFeaturePoints, .showAnchorOrigins, .showAnchorGeometry]
#endif
//AnchorEntity Bild
let anchor = AnchorEntity(.image(group: "AR Resources", name: "Test"))
// Create an image anchor by specifying the group name and image name of the AR resource
let box = ModelEntity(mesh: .generateBox(size: simd_make_float3(0.1, 0.03, 0.05)))
anchor.addChild(box)
view.scene.anchors.append(anchor)
//End AnchorEntity
return view
}
func updateUIView(_ view: ARView, context: Context) {
}
}
It's a code snippet from my Medium story.
This is done differently from Xcode, but no more complicated than in Xcode. Use + button in the upper right corner of your Swift Playgrounds app to place your reference images there.
Then use this code.
import ARKit
let sceneView = ARSCNView(frame: .zero)
var trackingImages = Set<ARReferenceImage>()
fileprivate func feedSession() {
let imageFromWeb = #imageLiteral(resourceName: "Photo.png")
let image = ARReferenceImage(imageFromWeb.cgImage!, orientation: .up,
physicalWidth: 0.5)
self.trackingImages.insert(image)
let config = ARImageTrackingConfiguration()
config.trackingImages = trackingImages
self.sceneView.session.run(config)
}

Use NSItemProvider in combination with NSFilePromiseProvider

How can I use a NSFilePromiseProvider in combination with .onDrag in SwiftUI? It only allows me to return an NSItemProvider, but for this, the file must already exist on the computer as the callback of registerFileRepresentation is called eagerly.
I want to drag a remote file displayed in my application to my computer desktop. With AppKit I used file promises for this use case, but for SwiftUI I found no solution so far.
struct ContentView: View {
var body: some View {
Text("Draggable Text")
.onDrag {
let provider = NSItemProvider()
provider.registerFileRepresentation(
forTypeIdentifier: UTType.fileURL.identifier,
fileOptions: [],
visibility: .all
) {
// This file must already exists. How to provide a file promise here?
let fileUrl = URL(fileURLWithPath: "file:///path/to/file.txt")
$0(fileUrl, true, nil)
return nil
}
return provider
}
}
}
Here's a limited workaround. It uses a FileHandle to write to the created file even after the drop has finished.
It reliably works for drop targets that move the file (e.g. Finder).
It fails for drop targets that read from the file (e.g. Apple Mail) if the file hasn't been completely written when the drop event occurs. In this situation, the empty placeholder content is read by the target app.
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Drag me")
.onDrag {
let provider = NSItemProvider()
provider.registerDataRepresentation(for: .fileURL, visibility: .all, loadHandler: loadFileForItemProvider)
return provider
}
}
#Sendable
func loadFileForItemProvider(onComplete callback: #escaping (Data?, Error?) -> Void) -> Progress? {
let progress = Progress(totalUnitCount: 100)
Task.detached {
do {
let temporaryDirectory = try FileManager.default.url(for: .itemReplacementDirectory, in: .userDomainMask, appropriateFor: .documentsDirectory, create: true)
let url = temporaryDirectory.appendingPathComponent("some_file.pdf")
try Data().write(to: url) // create temporary, emptyfile
let handle = try FileHandle(forWritingTo: url)
callback(url.dataRepresentation, nil)
let fileData = try await ... // load/generate file data here
try handle.write(contentsOf: fileData)
try? handle.close()
progress.completedUnitCount = 100
} catch {
callback(nil, error)
}
}
return progress
}
}
Remember to clean up the temporary directory after a certain duration or when quittung/launching the app.

How to programmatically export 3D mesh as USDZ using ModelIO?

Is it possible to programmatically export 3D mesh as .usdz file format using ModelIO and MetalKit frameworks?
Here's a code:
import ARKit
import RealityKit
import MetalKit
import ModelIO
let asset = MDLAsset(bufferAllocator: allocator)
asset.add(mesh)
let filePath = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask).first!
let usdz: URL = filePath.appendingPathComponent("model.usdz")
do {
try asset.export(to: usdz)
let controller = UIActivityViewController(activityItems: [usdz],
applicationActivities: nil)
controller.popoverPresentationController?.sourceView = sender
self.present(controller, animated: true, completion: nil)
} catch let error {
fatalError(error.localizedDescription)
}
When I press a Save button I get an error.
The Andy Jazz answer is correct, but needs modification in order to work in a SwiftUI Sandboxed app:
First, the SCNScene needs to be rendered in order to export correctly. You can't create a bunch of nodes, stuff them into the scene's root node and call write() and get a correctly rendered usdz. It must first be put on screen in a SwiftUI SceneView, which causes all the assets to load, etc. I suppose you could instantiate a SCNRenderer and call prepare() on the root node, but that has some extra complications.
Second, the Sandbox prevents a direct export to a URL provided by .fileExporter(). This is because Scene.write() works in two steps: it first creates a .usdc export, and zips the resulting files into a single .usdz. The intermediate files don't have the write privileges the URL provided by .fileExporter() does (assuming you've set the Sandbox "User Selected File" privilege to "Read/Write"), so Scene.write() fails, even if the target URL is writeable, if the target directory is outside the Sandbox.
My solution was to write a custom FileWrapper, which I return if the WriteConfiguration UTType is .usdz:
public class USDZExportFileWrapper: FileWrapper {
var exportScene: SCNScene
public init(scene: SCNScene) {
exportScene = scene
super.init(regularFileWithContents: Data())
}
required init?(coder inCoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func write(to url: URL,
options: FileWrapper.WritingOptions = [],
originalContentsURL: URL?) throws {
let tempFilePath = NSTemporaryDirectory() + UUID().uuidString + ".usdz"
let tempURL = URL(fileURLWithPath: tempFilePath)
exportScene.write(to: tempURL, delegate: nil)
try FileManager.default.moveItem(at: tempURL, to: url)
}
}
Usage in a ReferenceFileDocument:
public func fileWrapper(snapshot: Data, configuration: WriteConfiguration) throws -> FileWrapper {
if configuration.contentType == .usdz {
return USDZExportFileWrapper(scene: scene)
}
return .init(regularFileWithContents: snapshot)
}
08th January 2023
At the moment iOS developers still can export only .usd, .usda and .usdc files; you can check this using canExportFileExtension(_:) type method:
let usd = MDLAsset.canExportFileExtension("usd")
let usda = MDLAsset.canExportFileExtension("usda")
let usdc = MDLAsset.canExportFileExtension("usdc")
let usdz = MDLAsset.canExportFileExtension("usdz")
print(usd, usda, usdc, usdz)
It prints:
true true true false
However, you can easily export SceneKit's scenes as .usdz files using instance method called: write(to:options:delegate:progressHandler:).
let path = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask)[0]
.appendingPathComponent("file.usdz")
sceneKitScene.write(to: path,
options: nil,
delegate: nil,
progressHandler: nil)

Saving WebView to PDF returns blank image?

I'm trying to figure out how to save a WebView to a PDF and totally stuck, would really appreciate some help?
I'm doing this in Cocoa & Swift on OSX, here's my code so far:
import Cocoa
import WebKit
class ViewController: NSViewController {
override func loadView() {
super.loadView()
}
override func viewDidLoad() {
super.viewDidLoad()
loadHTMLString()
}
func loadHTMLString() {
let webView = WKWebView(frame: self.view.frame)
webView.loadHTMLString("<html><body><p>Hello, World!</p></body></html>", baseURL: nil)
self.view.addSubview(webView)
createPDFFromView(webView, saveToDocumentWithFileName: "test.pdf")
}
func createPDFFromView(view: NSView, saveToDocumentWithFileName fileName: String) {
let pdfData = view.dataWithPDFInsideRect(view.bounds)
if let documentDirectories = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).first {
let documentsFileName = documentDirectories + "/" + fileName
debugPrint(documentsFileName)
pdfData.writeToFile(documentsFileName, atomically: false)
}
}
}
It's pretty simple, what I'm doing is creating a WebView and writing some basic html content to it which renders this:
And then takes the view and saves it to a PDF file but that comes out blank:
I've tried grabbing the contents from the webView and View but no joy.
I've found a similar problem here How to take a screenshot when a webview finished rending regarding saving the webview to an image, but so far no luck with an OSX Solution.
Could it be something to do with the document dimensions?
or that the contents is in a subview?
maybe if you capture the View you can't capture the SubView?
Any ideas?
iOS 11.0 and above, Apple has provided following API to capture snapshot of WKWebView.
#available(iOS 11.0, *)
open func takeSnapshot(with snapshotConfiguration: WKSnapshotConfiguration?, completionHandler: #escaping (UIImage?, Error?) -> Swift.Void)
Sample usage:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
if #available(iOS 11.0, *) {
webView.takeSnapshot(with: nil) { (image, error) in
//Do your stuff with image
}
}
}
iOS 10 and below, UIWebView has to be used to capture snapshot. Following method can be used to achieve that.
func webViewDidFinishLoad(_ webView: UIWebView) {
let image = captureScreen(webView: webView)
//Do your stuff with image
}
func captureScreen(webView: UIWebView) -> UIImage {
UIGraphicsBeginImageContext(webView.bounds.size)
webView.layer.render(in: UIGraphicsGetCurrentContext()!)
let image: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
Here's another relevant answer
So I kind of figured out how to solve it, it turns out you can't (especially on OSX) access and print a webview from a WKWebView.
You have to use a WebView and NOT a WKWebView (I originally started with WKWebView because a few of the articles I read said to use that).
A WebView object is pretty much similar to a WKWebView object, which is fun as hell :-)
But it gives you access to .mainFrame & .frameView which you'll need to print it's content.
Here's my code:
let webView = WebView(frame: self.view.frame)
let localfilePath = NSBundle.mainBundle().URLForResource(fileName, withExtension: "html");
let req = NSURLRequest(URL: localfilePath!);
webView.mainFrame.loadRequest(req)
self.view.addSubview(webView)
Once it's rendered I then added a 1 second delay just to make sure the content has rendered before I print it,
// needs 1 second delay
let delay = 1 * Double(NSEC_PER_SEC)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(time, dispatch_get_main_queue()) {
// works!
let data = webView.dataWithPDFInsideRect(webView.frame)
let doc = PDFDocument.init(data: data)
doc.writeToFile("/Users/john/Desktop/test.pdf")
// works!
let printInfo = NSPrintInfo.sharedPrintInfo()
let printOperation = NSPrintOperation(view: webView.mainFrame.frameView, printInfo: printInfo)
printOperation.runOperation()
}
Here I'm printing it and saving it as a PDF, just so I'm doubly sure it works in all circumstances.
I'm sure it can be improved, I hate the delay hack, should replace that with some kind of callback or delegate to run when the content has fully loaded.