My app shows a list of files (images, videos, audios, texts), similar to Finder.
I use LazyVGrid, instead of List or Table views, because I want to render a grid, not just rows.
I want to enable dragging items from my app to other apps. Some other apps (e.g. Finder) require fileURL in the drop item provider, while others require data e.g. image (afaik e.g. Figma).
I want to make it possible to drag multiple items at a time, which afaik SwiftUI's onDrag doesn't support yet (the callback has to return exactly one NSItemProvider) unless it's a List or Table view.
So I use an NSHostingView:
class MultiDragNSHostingView<Content>: NSHostingView<Content> where Content: View {
let fileURL: URL
let selectedFileURLs: [URL]
// ...
override func mouseDragged(with event: NSEvent) {
beginDraggingSession(with: [selectedFileURLs.map { url in getDraggingItem(for: url) }], event: event, source: self)
super.mouseDragged(with: event)
}
private func getDraggingItem(for url: URL) -> NSDraggingItem {
// ???
}
}
If I used onDrag { NSItemProvider(contentsOf: fileURL) }, then for a PNG image, the drop item provider would contain these registeredTypeIdentifiers: ["public.png", "public.file-url"].
My question is: How should I implement getDraggingItem above, to get drop item providers similar to when using onDrag, i.e. having both "public.file-url" and "public.png" (or other image/video/audio/text) registered type identifiers?
I checked the initializer for NSDraggingItem, it has one param pasteboardWriter: NSPasteboardWriting. Conforming types are e.g. NSFilePromiseProvider and NSImage, but I don't see yet what pasteboardWriter I need to answer my question above.
Related
I've trying to reorder objects from at TableView in a Realm utility class. Many other Stack Overflow questions have said to uses List, however I can't seem to make it work. (Example, example). I'm able to successfully add objects to my list with:
public func addUserSong(song: Song) {
let songList = List<Song>()
songList.append(objectsIn: realm.objects(Song.self))
try! realm.write {
songList.append(song)
realm.add(songList)
}
}
However, I'm not able to preserve the updated order when trying:
public func reorder(from: Int, to: Int) {
let songs = List<Song>()
songs.append(objectsIn: realm.objects(Song.self))
songs.move(from: from, to: to)
try! realm.write {
realm.add(songs)
}
My models are:
class Song: Object {
#Persisted var name: String
#Persisted var key: String
}
class SongList: Object {
let songs = List<Song>()
}
Thanks!
Realm object order is not guaranteed. (unless you specify a sort order)
e.g. if you load 10 songs from Realm, they could come into your app an any order and the order could change between loads. The caveat to that is a Realm List object. Lists always maintain their order.
The problem in the question is you have Song objects stored in Realm but as mentioned above there is no ordering.
So the approach needs to be modified by leveraging a List object for each user to keep track of their songs:
class UserClass: Object {
#Persisted var name: String
#Persisted var songList = List<SongClass>()
}
When adding a song to a user, call it within a write transaction
try! realm.write {
someUser.songList.append(someSong)
}
suppose the user wants to switch the place of song 2 and song 3. Again, within a write transaction:
try! realm.write {
someUser.songList.move(from: 2, to: 3)
}
So then the UI bit - tableViews are backed by a tableView dataSource - this case it would be the songList property. When that dataSource is updated, the tableView should reflect that change.
In this case you would add an observer to the someUser.songList and as the underlying data changes, the observer will react to that change and can then update the UI.
You can do something simple like tableView.reloadData() to reflect the change or if you want fine-grained changes (like row animations for example) you can do that as well. In that same guide, see the code where tableView.deleteRows, .insertRows and .reload is handled. You know what rows were changed in the underlying data there so you can then animate the rows in the tableView.
I'm trying to add some drag-and-drop support to my SwiftUI macOS app to allow the user to drop .stl files onto the window. I’ve got the drop working, but I only seem to be able to validate against public.file-url, rather than the specific public.standard-tesselated-geometry-format (provided by ModelIO as kUTTypeStereolithography).
In my code, I do something like this:
func
validateDrop(info inInfo: DropInfo)
-> Bool
{
debugLog("STL: \(kUTTypeStereolithography)")
let has = inInfo.hasItemsConforming(to: [kUTTypeFileURL as String, kUTTypeData as String])
return has
}
func
performDrop(info inInfo: DropInfo)
-> Bool
{
inInfo.itemProviders(for: [kUTTypeFileURL as String, kUTTypeData as String, kUTTypeStereolithography]).forEach
{ inItem in
debugLog("UTIs: \(inItem.registeredTypeIdentifiers)")
inItem.loadItem(forTypeIdentifier: kUTTypeFileURL as String, options: nil)
{ (inURLData, inError) in
if let error = inError
{
debugLog("Error: \(error)")
return
}
if let urlData = inURLData as? Data,
let url = URL(dataRepresentation: urlData, relativeTo: nil)
{
debugLog("Dropped '\(url.path)'")
}
}
}
return true
}
This works well enough if I look for kUTTypeFileURL, but not if I look for kUTTypeStereolithography when I drop a .stl file onto the view.
I tried declaring it as an imported type but a) that didn’t seem to work and b) shouldn’t be necessary, since the existence of a symbolic constant implies macOS know about the type. I also don’t really know what it conforms to, although I tried both public.data and public.file-url.
I’d really like to be able to validate the type so that the user isn’t mislead by the drop UI. Currently, it indicates that my view will accept the drop if it’s not an .stl file, which I don’t want it to do.
This question deals with tab window restoration in a document-based app.
In an OSX, document-based app, which allows a user to create and convert tab windows, I need to preserve and restore the 'tab' state of each window.
Currently, my document controller restores its documents windows, but not the tab deployment; I get back individual windows; I can merge all back into one, but this is too heavy-handed as their former groupings are lost.
My app document class's - makeWindowControllers() function is where I affect the new controllers, whether they should cascade, which I'd read be false, during restore:
// Determine cascade based on state of application delegate
controller.shouldCascadeWindows = <app did receive applicationWillFinishLaunching>
so it would be false until it's finished launching.
Finally, my window's class features methods:
override func addTabbedWindow(_ window: NSWindow, ordered: NSWindow.OrderingMode) {
super.addTabbedWindow(window, ordered: ordered)
window.invalidateRestorableState()
}
override func moveTabToNewWindow(_ sender: Any?) {
super.moveTabToNewWindow(sender)
self.invalidateRestorableState()
}
override func encodeRestorableState(with coder: NSCoder) {
if let tabGroup = self.tabGroup {
let tabIndex = tabGroup.windows.firstIndex(of: self)
coder.encode(tabIndex, forKey: "tabIndex" )
Swift.print("<- tabIndex: \(String(describing: tabIndex))")
}
}
override func restoreState(with coder: NSCoder) {
let tabIndex = coder.decodeInt64(forKey: "tabIndex")
Swift.print("-> tabIndex: \(tabIndex)")
}
to invalidate the window restore state when the tab state is changed. But I'm not sure with the NSWindowRestoration protocol implementation, who or what needs to implement the protocol when a document controller is involved.
I think this is the reason the last function is never called. I get debug output about the encoding but during the next app execution the restoreStore(coder:) function is never called.
So who implements this window restore protocol in such an environment I guess is my question, or a decent example doing so.
My question reveals you not require anything special for a document based app; I've updated my prototype which features this support and environment here SimpleViewer, which features Swift5, a document based app supporting tabs.
I have File Promises implemented in a Cocoa app that allows dragging images from a view and dropping them to folders on the machine or to apps like preview or evernote. In the past this has worked well using the NSDraggingSource delegate and 'namesOfPromisedFilesDropped' method. This method would return the drop location and would allow me to write the image data directly there. So when dropping to an application icon like preview, or within an app like evernote, the file would be written and the app would either load or the image would simply show within.
Unfortunately this method was deprecated in 10.13 so it no longer gets called in new OS versions. Instead I've switched over to using the filePromiseProvider writePromiseTo url method of the "NSFilePromiseProviderDelegate" delegate. This method gets called, and the image data is processed. I get the destination URL and attempt to write the image data to this location. This works perfectly fine when simply dragging to folders on the Mac. But, when dragging to other app icons like Preview.app, or directly to a folder in Evernote, I get an error 'Error Domain=NSPOSIXErrorDomain Code=2 "No such file or directory"'.
I've attempted this using full URLs, and, URL paths. Regardless of either, when dragging to other apps it simply will not allow the drop or creation of the file at that location.
Is there some entitlement that may be missing? I've even attempted the Apple source code found here with the same error: File Promises Source
Here is the code I'm using now that's returning the error with inability to write to outside app locations. This only works for dragging to folders on the computer.
extension DragDropContainerView: NSFilePromiseProviderDelegate {
internal func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, fileNameForType fileType: String) -> String {
let fileName = NameFormatter.getFormattedNameFromDate() + "." + fileType.getFileType().typeIdentifier
return fileName
}
internal func operationQueue(for filePromiseProvider: NSFilePromiseProvider) -> OperationQueue {
return workQueue
}
internal func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, writePromiseTo url: URL, completionHandler: #escaping (Error?) -> Void) {
if let snapshot = filePromiseProvider.userInfo as? SnapshotItem {
if let data = snapshot.representationData {
do {
try data.write(to: url)
completionHandler(nil)
} catch {
completionHandler(error)
}
}
}
}
}
Any help on this issue would be great. Ultimately, I simply want to be able to drag the image to an app and have that app accept the drag. This used to work but no longer does.
Update 1:
After extensive experimentation I've managed to find a 'solution' that works. I don't see why this works but some how this ultimately kicks off a flow that fires the old 'deprecated' method.
I create the dragged item;
let draggingItem = NSDraggingItem(pasteboardWriter: pasteboardWriter(forImageCanvas: self))
draggingItem.setDraggingFrame(screenshotImageView.frame, contents: draggingImage)
beginDraggingSession(with: [draggingItem], event: event, source: self)
This calls the below method;
private func pasteboardWriter(forImageCanvas imageCanvas: DragDropContainerView) -> NSPasteboardWriting {
let provider = FilePromiseProvider(fileType: kUTTypeJPEG as String, delegate: self)
provider.userInfo = imageCanvas.snapshotItem
return provider
}
This sets up the custom file promise session using the subclass below. As you can see, I'm not handling any logic here and it seems to be doing very little or nothing. As an added test to verify it's not really doing much, I'm setting the pasteboard type to 'audio'. I'm dragging an image not audio but it still works.
public class FilePromiseProvider : NSFilePromiseProvider {
public override func writableTypes(for pasteboard: NSPasteboard)
-> [NSPasteboard.PasteboardType] {
return [kUTTypeAudio as NSPasteboard.PasteboardType]
}
public override func writingOptions(forType type: NSPasteboard.PasteboardType,
pasteboard: NSPasteboard)
-> NSPasteboard.WritingOptions {
return super.writingOptions(forType: type, pasteboard: pasteboard)
}
}
So long at the above methods are implemented it apparently kicks off a flow that ultimately calls the 'deprecated' method below. This method works every time perfectly in Mojave showing that there must be an issue with the NSFilePromises API. If this method works, the file promises API should work the same but it does not;
override func namesOfPromisedFilesDropped(atDestination dropDestination: URL) -> [String]?
Once this method gets called everything works perfectly in Mojave for drags to app icons in the dock and directly into applications like Evernote returning the app to 100% drag drop functionality as it used to work in previous OS versions!
My file promises delegate is still in place but looks like the below. As you can see it's not doing anything any longer. However it's still required.
extension DragDropContainerView: NSFilePromiseProviderDelegate {
func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, fileNameForType fileType: String) -> String {
return ""
}
func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, writePromiseTo url: URL, completionHandler: #escaping (Error?) -> Void) {
}
}
Any comments on this 'solution' would be great. Any suggestions on how to do this better would also be welcomed. Please note, that according to Apple, there "is no guarantee that the file will be written in time" using the File Promises API. But with the above, the old deprecated method is somehow called in Mojave and works flawlessly every time.
Thanks
I have a need to sync two web views, so that anything that happens in one web view happens simultaneously in the other.
I have tried various ways, without success, and the more I try the more convoluted and likely bug ridden this is getting.
I feel there maybe a very simple way to do this, but can not figure it out.
One of the things that I note is not allowed is having the same NSTextField as the "takeStringURLFrom"
override func controlTextDidEndEditing(obj: NSNotification) {
webViewLeft.takeStringURLFrom(hiddenTextField)
webViewRight.takeStringURLFrom(urlField)
}
override func webView(sender: WebView!, didCommitLoadForFrame frame: WebFrame!) {
if frame == sender.mainFrame {
urlField.stringValue = sender.mainFrameURL
hiddenTextField.stringValue = sender.mainFrameURL
webViewRight.takeStringURLFrom(urlField)
webViewLeft.takeStringURLFrom(hiddenTextField)
printLn("realised just creating an infinite loop here")
}
}
I don't like this but it appears to work as needed. I think it needs some refinement. Using a hidden text field to mimic the url for the second web view and tie each web view to there respective text fields via the web view's referenced action "takeStringUrlFrom"
override func webView(sender: WebView!, didStartProvisionalLoadForFrame frame: WebFrame!) {
if frame == sender.mainFrame {
urlField.stringValue = sender.mainFrameURL
hiddenTextField.stringValue = sender.mainFrameURL
window.makeFirstResponder(nil)
window.makeFirstResponder(hiddenTextField)
window.makeFirstResponder(nil)
window.makeFirstResponder(urlField)
window.makeFirstResponder(nil)
}
}