Thumbnail Metadata Disappeared When Importing File with Copy to App - ios

I have been using the URL Resources to embed the thumbnail metadata into my custom document-based file. When I export my custom document file, the thumbnail shows up nicely when glanced in iOS Files app.
override func fileAttributesToWrite(to url: URL, for saveOperation: UIDocumentSaveOperation) throws -> [AnyHashable : Any] {
var attr: [AnyHashable : Any] = [URLResourceKey.hasHiddenExtensionKey: true]
// Ignore the proper resizing for now.
...
attr[URLResourceKey.thumbnailDictionaryKey] = [
URLThumbnailDictionaryItem.NSThumbnail1024x1024SizeKey: #imageLiteral(resourceName: "Star")
]
return attr
}
(Icon Credit: Use Your Loaf)
However, when I import the file back into my app with Share action, Copy to <MyApp>, all the metadata seems to survive except the thumbnail.
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
// Other non-image metadata survives, like creationDate.
if let creationDateOpt = try? url.resourceValues(forKeys: [.creationDateKey]).creationDate,
let creationDate = creationDateOpt {
print("creationDate: \(creationDate)")
}
if let thumbnailDictOpt = try? url.resourceValues(forKeys: [.thumbnailDictionaryKey]).thumbnailDictionary,
let thumbnailDict = thumbnailDictOpt,
let thumbnail = thumbnailDict[URLThumbnailDictionaryItem.NSThumbnail1024x1024SizeKey] {
print ("Thumbnail survived. Size: \(thumbnail.size)")
return true
}
print ("Thumbnail disappeard!")
return false
}
Considering that the thumbnail persists when I manual copy the file in Files app, the lost must happen during the system copying my file from Files to my app's container. Why does this happen? I can manually embed thumbnail as part of my file's data too, but I feel there should be a way to solve this, since other text-based metadata persists.
I tried using promisedResourceValue method instead, suspecting that the file might not be fully copied when it's opened, but the result is the same.
I have my full project available here on GitHub.

Related

Export all images from Core Data SwiftUI

I'm trying to build out a simple way for my users to export their data outside of the app.. nothing that needs to be imported back in, just some way for them to back up the data for reference purposes. I have a Core Data Entity Project and the users are able to individually share a project in order to save the project data and images using the standard iOS Share Sheet. Works great.
However I'd like there to be a solution to export everything at once, not just individual projects one at a time.
I have part of it working, where I can export the data from Core Data (that isn't an image) into a CSV for users to reference. However I'm stuck on finding the best way to get all the Images exported in a similar singular button. Allowing the user to pick a location where a Folder would be created containing the images would be ideal.
Here's my code for the CSV export which works great:
func exportCSV() {
let fileName = "Metadata_Export_\(Date()).csv"
let path = NSURL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName)
var csvText = "Name,Date,Project_Description\n"
for project in projects {
csvText += "\(project.person?.name ?? "-"),\(project.date ?? Date()),\(project.bodyText ?? "-"),\n"
}
do {
try csvText.write(to: path!, atomically: true, encoding: String.Encoding.utf8)
} catch {
print("Failed to create file")
print("\(error)")
}
print(path ?? "not found")
var filesToShare = [Any]()
filesToShare.append(path!)
let av = UIActivityViewController(activityItems: filesToShare, applicationActivities: nil)
UIApplication.shared.windows.first?.rootViewController?.present(av, animated: true, completion: nil)
isShareSheetShowing.toggle()
}
Now I just need to get the Images exported out. Images are saved in Core Data as Binary objects, and will be written to File if they're larger than 128kb (and therefore written to blob in CD if less than 128kb).
The images are stored in CD as Optionals, project.image1, project.image2, project.image3, and project.image4
I've looked at examples using fileManager and other solutions, but I'm not sure on the correct approach to pursue since many of those are actually alternatives to saving images in Core Data - not necessarily configuring user interaction for picking where to export images.
Can the above exportCSV function be adapted to a similar result for the project's images? My app supports iOS 14 and later, if that makes a difference. Thanks for any suggestions/direction!
=== UPDATE ===
I've discovered fileExporter() which seems like a promising solution. I've been able to implement a simple POC of this method by exporting an Image I have stored in my Assets folder. Has anyone used this method to achieve exporting all images out of Core Data?
I can add the modifier to my view:
.fileExporter(isPresented: $exportFile, documents: [
ImageDocument(image: UIImage(named: "testimage"))
],
contentType: .png, onCompletion: { (result) in
if case .success = result {
print("Success")
} else {
print("Failure")
}
})
}
Using an ImageDocument Struct as follows:
struct ImageDocument: FileDocument {
static var readableContentTypes: [UTType] { [.jpeg, .png, .tiff] }
var image: UIImage
init(image: UIImage?) {
self.image = image ?? UIImage()
}
init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents,
let image = UIImage(data: data)
else {
throw CocoaError(.fileReadCorruptFile)
}
self.image = image
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
// You can replace tiff representation with what you want to export
return FileWrapper(regularFileWithContents: image.jpegData(compressionQuality: 1)!)
}
}
So how can I have it include an Array of all images?

Share PHAsset stored in camera roll using UIActivityViewController

I'm storing images and videos in a Camera Roll album using PhotoKit, and want to allow the user to share them using UIActivityViewController. If I pass UIActivityViewController a UIImage instance, it works as expected, probably because the image data is passed in memory. However, videos need to be passed by URL because there's no video analogue to UIImage. When I pass a URL to a video, I get an error "Could not create sandbox extension". If I pass a URL to an image, I get a similar error.
Based on this, it seems as though I might be able to get around this error by exporting the assets to the Documents directory, and passing UIActivityViewController the URL to the asset in Documents. However, I've read elsewhere that the Camera Roll can serve a similar purpose, and it goes to reason that the Camera Roll would be one of the few places that can hold data for sharing between apps.
Is there a way to pass UIActivityViewController URLs to Camera Roll assets without copying them to Documents? Is there a better way to be sharing images and video that are already in Camera Roll?
Implementation Details:
I'm generating URLs for assets using this:
func videoFor(asset: PHAsset, resultHander: #escaping (AVAsset?, AVAudioMix?, [AnyHashable : Any]?) -> Void) {
imageManager.requestAVAsset(forVideo: asset, options: nil, resultHandler: resultHander)
}
func urlFor(asset: PHAsset, resultHandler: #escaping (URL?) -> Void) {
if ( asset.mediaType == .video ) {
videoFor(asset: asset) { (asset, audioMix, info) in
let asset = asset as! AVURLAsset
resultHandler(asset.url)
}
}
else if ( asset.mediaType == .image ) {
let options: PHContentEditingInputRequestOptions = PHContentEditingInputRequestOptions()
options.canHandleAdjustmentData = {(adjustmeta: PHAdjustmentData) -> Bool in
return true
}
asset.requestContentEditingInput(with: options, completionHandler: {(contentEditingInput: PHContentEditingInput?, info: [AnyHashable : Any]) -> Void in
resultHandler(contentEditingInput!.fullSizeImageURL as URL?)
})
}
else {
resultHandler(nil)
}
}
Here is the full error I get in console when trying to share an image by URL:
Failed to determine whether URL /var/mobile/Media/DCIM/100APPLE/IMG_0201.JPG (n) is managed by a file provider
Could not create sandbox extension. Error: Error Domain=NSPOSIXErrorDomain Code=1 "Operation not permitted" UserInfo={NSLocalizedDescription=Could not create sandbox extension of type com.apple.app-sandbox.read for URL /var/mobile/Media/DCIM/100APPLE/IMG_0201.JPG. Error: No such file or directory}
... and for a video:
Failed to determine whether URL /var/mobile/Media/DCIM/100APPLE/IMG_0202.M4V (n) is managed by a file provider
Could not create sandbox extension. Error: Error Domain=NSPOSIXErrorDomain Code=1 "Operation not permitted" UserInfo={NSLocalizedDescription=Could not create sandbox extension of type com.apple.app-sandbox.read for URL /var/mobile/Media/DCIM/100APPLE/IMG_0202.M4V. Error: Operation not permitted}
I was stuck on the same problem today. Here is my solution. Hope this helps or guides you to the right path.
PHImageManager.default().requestExportSession(forVideo: video, options: nil, exportPreset: AVAssetExportPresetPassthrough) { (exportSession, nil) in
if let exportSession = exportSession {
exportSession.outputURL = destinationURLForFile
exportSession.outputFileType = AVFileType.m4v
exportSession.exportAsynchronously() {
// Load the share sheet using destinationURLForFile
}
}
}
What this does is export the video to the provided location destinationURLForFile (i used the Documents directory. Make sure you delete the file if its already there otherwise the export MAY not work cause it may not override the file).
You can set the type based on available types. I needed m4v.
Then, export async and just call the share sheet or whatever sharing mechanism you have.

SCNParticleSystem load from document Directory

I am trying load SCNParticleSystem from download bundle which i am not able to load.
Path for the resource.
file:///var/mobile/Containers/Data/Application/A91E9970-CDE1-43D8-B822-4B61EFC6149B/Documents/so/solarsystem.bundle/Contents/Resources/
let objScene = SCNParticleSystem(named: "stars", inDirectory: directory)
This object is nil.
This is a legitimate problem since SceneKit does not provide an out-of-the-box solution for initializing particle systems from files that are outside of the main bundle (the only init method SCNParticleSystem.init(named:inDirectory:) implies that SCNParticleSystem.scnp files are in the main bundle).
Luckily for us .scnp files are just encoded/archived SCNParticleSystem instances that we can easily decode/unarchive using NSKeyedUnarchiver:
extension SCNParticleSystem {
static func make(fromFileAt url: URL) -> SCNParticleSystem? {
guard let data = try? Data(contentsOf: url),
let object = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data),
let system = object as? SCNParticleSystem else { return nil }
return system
}
}
If you do not need to support iOS 9 and iOS 10 you can use NSKeyedUnarchiver.unarchivedObject(ofClass: SCNParticleSystem.self, from: data) instead of NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(_:) and type casting, which was introduced in iOS 11.0.
Another issue that you're most likely to encounter is missing particle images. That is because by default SceneKit will look for them in the main bundle. As of current versions of iOS (which is iOS 12) and Xcode (Xcode 10) particle images in .scnp files (particleImage property) are String values which are texture filenames in the main bundle (that might change, but probably won't, however there's not much else we could use).
So my suggestion is to take that filename and look for the texture file with the same name in the same directory where the .scnp file is:
extension SCNParticleSystem {
static func make(fromFileAt url: URL) -> SCNParticleSystem? {
guard let data = try? Data(contentsOf: url),
let object = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data),
let system = object as? SCNParticleSystem else { return nil }
if let particleImageName = system.particleImage as? String {
let particleImageURL = url
.deletingLastPathComponent()
.appendingPathComponent(particleImageName)
if FileManager.default.fileExists(atPath: particleImageURL.path) {
system.particleImage = particleImageURL
}
}
return system
}
}
You can just set the URL of the image file and SceneKit will handle it from there.
As a little side-note, the recommended directory for downloadable content is Application Support directory, not Documents.
Application Support: Use this directory to store all app data files except those associated with the user’s documents. For example, you might use this directory to store app-created data files, configuration files, templates, or other fixed or modifiable resources that are managed by the app. An app might use this directory to store a modifiable copy of resources contained initially in the app’s bundle. A game might use this directory to store new levels purchased by the user and downloaded from a server.
(from File System Basics)
Don't have enough reps to add the comment so adding it as the answer.
The answer by Lësha Turkowski works for sure but was had issues with loading the particle images using only NSURL.
All particles were appearing square which meant,
If the value is nil (the default), SceneKit renders each particle as a
small white square (colorized by the particleColor property).
SCNParticleSystem particleImage
In the documentation it says You may specify an image using an
NSImage (in macOS) or UIImage (in iOS) instance, or an NSString or
NSURL instance containing the path or URL to an image file.
Instead of using the NSURL, ended up using the UIImage and it loaded up fine.
extension SCNParticleSystem {
static func make(fromFileAt url: URL) -> SCNParticleSystem? {
guard let data = try? Data(contentsOf: url),
let object = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data),
let system = object as? SCNParticleSystem else { return nil }
if let particleImageName = system.particleImage as? String {
let particleImageURL = url
.deletingLastPathComponent()
.appendingPathComponent(particleImageName)
if FileManager.default.fileExists(atPath: particleImageURL.path) {
// load up the NSURL contents in UIImage
let particleUIImage = UIImage(contentsOfFile: particleImageURL.path)
system.particleImage = particleUIImage
}
}
return system
}
}
I found out, that sometimes when dragging a SCNParticleSystem file into your project (probably form a different project) a silent error can happen due to some bugs in Xcode. As a result you can't get a reference to an instance of your SCNParticleSystem.
Solution: Check your BuildSettings in your target. The SCNPaticleSystem AND the associated ImageFile should be listed there and then you should get it right. (see screenShot below)

How to use my view controllers and other class in the Share Extension ? iOS | Swift 4

I am creating a chatting application. User can share the images from other application to my application. I have added Share Extension to show my app in the native share app list. I'm also getting the selected data in didSelectPost Method. From here I want to show the list of the users to whom the image can be forwarded. For this, I'm using an already created view controller in the main app target.
override func didSelectPost() {
// This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
if let content = self.extensionContext!.inputItems[0] as? NSExtensionItem {
let contentType = kUTTypeImage as String
// Verify the provider is valid
if let contents = content.attachments as? [NSItemProvider] {
for attachment in contents {
if attachment.hasItemConformingToTypeIdentifier(contentType) {
attachment.loadItem(forTypeIdentifier: contentType, options: nil) { (data, error) in
let url = data as! URL
let imageData = try! Data(contentsOf: url)
// Here I'm navigating to my viewcontroller, let's say: ForwardVC
}
}
}
}
}
I don't want to recreate the same screen in Share Extension. Apart from this view controllers, I have many more classes and wrappers that I want to use within the share extension. Like, SocketManager, Webservices, etc. Please suggest me your approach to achieve the same.
P.S.: I've tried setting multiple targets to required viewControllers and using same pods for Share Extention. In this approach, I'm facing a lot of issues as many of the methods and pods are not extention compliant. Also, is it the right way to do this.

UNNotificationServiceExtension's didRecieve not called

I moved step by step for getting rich push notifications. Here they are :
Created Notification service extension with plist :
NotificationService didRecieve :
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: #escaping (UNNotificationContent) -> Void) {
func failEarly() {
contentHandler(request.content)
}
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
// Get the custom data from the notification payload
if let data = request.content.userInfo as? [String: AnyObject] {
// Grab the attachment
// let notificationData = data["data"] as? [String: String]
if let urlString = data["attachment-url"], let fileUrl = URL(string: urlString as! String) {
// Download the attachment
URLSession.shared.downloadTask(with: fileUrl) { (location, response, error) in
if let location = location {
// Move temporary file to remove .tmp extension
let tmpDirectory = NSTemporaryDirectory()
let tmpFile = "file://".appending(tmpDirectory).appending(fileUrl.lastPathComponent)
let tmpUrl = URL(string: tmpFile)!
try! FileManager.default.moveItem(at: location, to: tmpUrl)
// Add the attachment to the notification content
if let attachment = try? UNNotificationAttachment(identifier: "video", url: tmpUrl, options:nil) {
self.bestAttemptContent?.attachments = [attachment]
}else if let attachment = try? UNNotificationAttachment(identifier: "image", url: tmpUrl, options:nil) {
self.bestAttemptContent?.attachments = [attachment]
}else if let attachment = try? UNNotificationAttachment(identifier: "audio", url: tmpUrl, options:nil) {
self.bestAttemptContent?.attachments = [attachment]
}else if let attachment = try? UNNotificationAttachment(identifier: "image.gif", url: tmpUrl, options: nil) {
self.bestAttemptContent?.attachments = [attachment]
}
}
// Serve the notification content
self.contentHandler!(self.bestAttemptContent!)
}.resume()
}
}
}
Configured AppId and provision profile for extension.
Rich notification is coming correctly :
But here are the issues I am facing :
didRecieve is not getting called. For that I attached the serviceExtension process to the app target and ran the app.
Note : Extension is getting called as soon as notification arrives but didRecieve is not called :
On opening the push notification (which has video attachment), nothing happens. Ideally it should get played.
If I have to open the video and play it, do I have to explicitly do something or extension will take care of that ?
Payload :
aps = {
alert = "This is what your message will look like! Type in your message in the text area and get a preview right here";
badge = 1;
"mutable-content" = 1;
sound = default;
};
"attachment-url" = "https://www.sample-videos.com/video/mp4/720/big_buck_bunny_720p_1mb.mp4";
deeplinkurl = "";
"message_id" = 1609;
}
I did try going through following posts but that didn't help :
iOS10 UNNotificationServiceExtension not called
NotificationServiceExtension not called
UNNotificationServiceExtension not working on iPhone 5 (iOS 10)
Good news! Your service extension is indeed being called - the image on your notification is evidence of that. What is probably happening here is that you are unable to debug the extension using the workflow you are used to with applications.
Debugging notification extensions is not like debugging an app. Extensions are plug-ins to an iOS process outside your application. Just setting a breakpoint is not a reliable way to debug them. Instead:
Debugging A Notification Service Extension
Launch the app from Xcode or the device
In Xcode, select Attach To Process or PID By Name... from the Debug menu
Enter the name of your notification extension
Trigger a notification (by sending a push, etc.).
When the notification is delivered the service extension should launch in to the debugger. Service extensions are only relevant to remote (push) notifications, so you will need a device to troubleshoot them.
Debugging A Notification Content Extension
There are at least two ways. The steps shown above for a service extension also work for a content extension. The second method is more familiar but less reliable.
Select the extension scheme in Xcode using the toolbar
In the Product menu, select Edit Scheme...
Set the Executable to the parent application.
Set a breakpoint inside the content extension.
Now build and run your extension. It will launch the parent application.
Trigger a notification that will cause the content extension to load.
It's worth noting that adding logging using the logging framework can be very useful for debugging and troubleshooting as well.
Why The Video May Not Be Playing
iOS limits the size of content that can be presented in notifications. This is described in the documentation for UNNotificationAttachment. For video it is generally 50Mb. Make sure your video is as small as you can make it in terms of bytes, and of course provide a video that is sized appropriately for the device it will be played on. Do not try to play a 1080p video in a notification that is 400 points wide!
In practice it is almost always better to use HLS instead of downloading video, and present it in a content extension.
Another thing in your code that may be problematic is the identifiers you are assigning to your attachments. Identifiers should be unique. Typically this would be a reverse-domain notation string like your bundle ID followed by a UUID string. You could also use the original URL of the content followed by a UUID string. If you provide an empty string iOS will create a unique identifier for you.
With the user notifications framework having non-unique identifiers (for notifications, attachments, etc.) tends to cause difficult to track down issues inside the framework. For example, this can cause an attached watchOS device to crash.
If you want to implement "auto play" for your video - it is not clear from your question wether that is what you are describing - you will need to implement your own player functionality in a content extension.
If you are going to do that, again, HLS is the preferred way to display video in a notification. It usually uses less RAM, offers a better user experience and tends to be more stable.

Resources