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
Related
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.
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.
I'm working on a school project where I need to implement OAuth2 to get access to our student profiles and display them. The app has two views - a search bar and detailed user info on the next page. I'm fairly new to this, so any advice will be greatly appreciated.
I was mostly following this tutorial https://grokswift.com/alamofire-OAuth2/ but a lot of stuff is deprecated. I can easily get all data using postman and user credentials but when I implement OAuth2 I can't parse the code I receive. So all my functions are in ContentView (pretty sure it's not the right way, but I couldn't make it work otherwise for now). I have a search button that looks like this:
Button(
action: {self.handleAPI()},
label: {
Text("Search")
}
And a handleAPI function that calls other functions, and func startOAuth2Login() among them:
if let authURL:NSURL = NSURL(string: authPath)
{
UIApplication.shared.open(authURL as URL, options: [:], completionHandler: nil)
print("here1")
}
Everything works as expected, redirects me to the login screen, where I enter my credentials, press "Authorize" and successfully return to the app. The print statement is also visible. However, I can't find the link I receive to parse the auth code. I know it should be in AppDelegate and this is what I have for now but it's not being called.
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
ContentView.sharedInstance.processOAuthStep1Response(url: url as NSURL)
print("url: \(url)")
return true
}
processOAuthStep1Response(url) is just another function in my handleAPI main function. I also have this line in my ContentView : static let sharedInstance = ContentView().
So the question is how to call my function that will parse the code? How can I even see this code? XCode doesn't give any errors and everything works, just after "here1" nothing else is called. Thank you in advance!
I have a viewMode that determines if iCloud is enabled or disabled with the result being a prompt to the User to login to iCloud or not.
Is there a way to progamatically login/logout to iCloud from an XCTest to reliable test all paths?
Here is my test
func testShowLoginButtonForiCloud() {
let viewModel = OnboardingViewModel()
let expectation = XCTestExpectation(description: "Wait for CKContainer auth check")
var iCloudEnabled: Bool?
viewModel.shouldShowiCloudLogin { result, error in
iCloudEnabled = result
expectation.fulfill()
}
wait(for: [expectation], timeout: 5.0)
XCTAssertNotNil(iCloudEnabled)
XCTAssertFalse(iCloudEnabled!)
}
Here is my ViewModel
typealias Completion = (Bool, Error?) -> Void
final class OnboardingViewModel {
func shouldShowiCloudLogin(completion: #escaping Completion) {
CKContainer.default().accountStatus { (status, error) in
switch status {
case .available :
completion(true, nil)
default :
completion(false, error)
}
}
}
}
Can we programmatically log in to CloudKit for unit testing? This is inadvisable, because even if we could, the tests would be slow & fragile. Instead, treat CloudKit as an architectural boundary. Unit tests can go right up to this boundary. And we can pretend stuff comes back from the boundary. In this way, we can test all paths.
To program this boundary into your code, use a protocol. This protocol will be a slice containing only those CKContainer methods you want. (This is the Interface Segregation Principle in action.) Since CKContainer already implements this method, we can attach it as an empty extension.
protocol CKContainerProtocol {
func accountStatus(completionHandler: #escaping (CKAccountStatus, Error?) -> Void)
}
extension CKContainer: CKContainerProtocol {}
Then add a property to your view model:
var cloudKitContainer: CKContainerProtocol = CKContainer.default()
The default value means your code will continue to use the real CKContainer unless told otherwise. Change your code to call cloudKitContainer instead of CKContainer.default().
Then in test code, you can provide a different implementation of CKContainerProtocol. This will let you do stubbing and mocking. You can confirm that accountStatus() is called exactly once. And you can exercise its closure with different CKAccountStatus values to confirm how your Completion closure is called.
I want my complication to get data from the iPhone via Watch Connectivity. I am using sendMessage Instant Messaging technology.
I don't want my iPhone app to be open when I try to get data, so this needs to work in the background.
In my ViewController on my iPhone:
import UIKit
import WatchConnectivity
class ViewController: UIViewController, WCSessionDelegate {
var session: WCSession!
override func viewDidLoad() {
super.viewDidLoad()
if WCSession.isSupported() {
self.session = WCSession.defaultSession()
self.session.delegate = self
self.session.activateSession()
}
}
func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
if message.count != 1 { return }
if message["request"] != nil {
replyHandler(["response" : "data"])
}
}
And in my ComplicationController
var session: WCSession!
func getCurrentTimelineEntryForComplication(complication: CLKComplication, withHandler handler: ((CLKComplicationTimelineEntry?) -> Void)) {
if complication.family != .ModularSmall {
handler(nil)
}
if WCSession.isSupported() {
self.session = WCSession.defaultSession()
self.session.delegate = self
self.session.activateSession()
}
var respondedString = "not"
session.sendMessage(["request" : ""], replyHandler: {
(resp) -> Void in
respondedString = resp["response"]
}, errorHandler: nil)
let circularTemplate = CLKComplicationTemplateModularSmallSimpleText()
circularTemplate.textProvider = CLKSimpleTextProvider(text: respondedString)
let timelineEntry = CLKComplicationTimelineEntry(date: NSDate(), complicationTemplate: circularTemplate)
handler(timelineEntry)
}
The only thing I can see on my Watch is "not". Why doesn't the complication show the received data?
The main issue is that you're trying to make an asynchronous call within your complication controller.
The code following your sendMessage: call will be executed before your reply handler has even gotten a response. This is why your complication shows "not" as the template's text has been set, before you have received a reply.
Sometime later, after getCurrentTimelineEntryForComplication has returned, sendMessage will receive a response and call the reply hander, which will merely set respondedString, then exit that block.
What you should avoid doing:
You should consider Apple's recommendations, and not try to fetch any data within the complication controller.
The job of your data source class is to provide ClockKit with any requested data as quickly as possible. The implementations of your data source methods should be minimal. Do not use your data source methods to fetch data from the network, compute values, or do anything that might delay the delivery of that data. If you need to fetch or compute the data for your complication, do it in your iOS app or in other parts of your WatchKit extension, and cache the data in a place where your complication data source can access it. The only thing your data source methods should do is take the cached data and put it into the format that ClockKit requires.
Also, any activity you perform within your data source will needlessly use up the daily execution time budget that is allotted to your complication.
How can you provide data to your complication?
Apple provides a Watch Connectivity transferCurrentComplicationUserInfo method which will immediately transfer (a dictionary of) complication info from the phone to the watch.
When your iOS app receives updated data intended for your complication, it can use the Watch Connectivity framework to update your complication right away. The transferCurrentComplicationUserInfo: method of WCSession sends a high priority message to your WatchKit extension, waking it up as needed to deliver the data. Upon receiving the data, extend or reload your timeline as needed to force ClockKit to request the new data from your data source.
On the watch side, you have your WCSessionDelegate handle didReceiveUserInfo and use the data you received to update your complication:
func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject]) {
if let ... { // Retrieve values from dictionary
// Update complication
let complicationServer = CLKComplicationServer.sharedInstance()
guard let activeComplications = complicationServer.activeComplications else { // watchOS 2.2
return
}
for complication in activeComplications {
complicationServer.reloadTimelineForComplication(complication)
}
}
}
Apple engineers generally recommend setting up a data manager to hold the data. In your complication controller, you would retrieve the latest information from the data manager to use for your timeline.
There are several existing projects on GitHub which use this approach.
If you still prefer to request data from the watch side:
You'd want to move your WCSession code out of the complication controller, into the watch extension, and activate it as part of the WKExtension init.
The key is to have the reply handler manually update the complication once the data is received.
When your session delegate's reply handler is called, you can use the update complication code I supplied earlier to reload your complication's timeline.
If you use scheduled complication updates to trigger this, the downside to that particular approach is that you'll be performing two updates. The first update would initiate the request for data, but not have any new data to use. The second (manual) update happens after the data is received, and this is when the new data would appear on the timeline.
This is why the approach of supplying data in the background from the phone works better, as it only requires one update.