swift + OS X sandboxing: treat 'NSVBOpenPanel' as a 'NSOpenPanel' :: because I need to get the sender in the delegate method - swift

Im using swift and I show a NSOpenPanel. In the delegate I need to look at the sender's prompt to distinguish which action to take:
e.g.
func show() {
...
panel.delegate = self
panel.prompt = "xy"
panel.run ....
}
func show2() {
...
panel.delegate = self
panel.prompt = "abc"
panel.run ....
}
//delegate
func panel(sender: AnyObject, shouldEnableURL url: NSURL) -> Bool {
let panelPrompt = (sender as! NSOpenPanel).prompt ...
}
without sandbox = WORKS fine
the sender of the delegate is a NSOpenPanel indeed
with sandbox = Cast fails, crash
the sender of the delegate is NOT a NSOpenPanel but a NSVBOpenPanel. Apple's private class that remotely speaks to the outside world and allows the user to choose files NORMALLY not in your sandbox. (for details I refer to apple's sandboxing guide)
So the question is how do I do use this in swift without crashing?
Is there a nice way or is it just a bug/ugly idk behavior
Do I have to revert to use performSelector?
===
Addition: extensions to NSOpenPanel don't work either!

Instead of casting the sender to NSOpenPanel (which fails because the
sender is an instance of the private NSVBOpenPanel class),
or some performSelector magic, you can use the fact that
arbitrary methods and properties can be accessed on AnyObject
without casting, and the call behaves like an implicitly
unwrapped optional:
func panel(sender: AnyObject, shouldEnableURL url: NSURL) -> Bool {
let panelPrompt = sender.prompt ?? ""
// ...
return true
}
This gives the prompt for any sender object which has a prompt
property, and the empty string as a fallback. In my test it worked well
in a sandboxed environment.
See The strange behaviour of Swift's AnyObject for more details, examples, and references to the
documentation.

This is how it would work with performSelector. It is quite ugly though:
let panelPromptUnmanaged = (sender as! NSObject).performSelector(NSSelectorFromString("prompt"))
let panelPrompt = panelPromptUnmanaged != nil ? panelPromptUnmanaged.takeRetainedValue() as! String : ""

Related

Weird behavior when I try to use a closure as the target of a UIButton

I know I need to replace the let keyword with lazy var for accessing the property otherwise I cannot access the 'self'.
But I found that the button.addTarget can build successfully as below,
Why? Normally if you try to access the property from a closure that needs to be a lazy variable, am I right?
For comparison, The testProperty shows red error message:
Cannot convert value of type (testController) -> () -> testController to specified type UITabBarController
import UIKit
class testController: UIViewController {
let actionButton: UIButton = {
let button = UIButton(type: .system)
button.addTarget(self, action: #selector(actionButtonTapped), for: .touchUpInside)
return button
}()
let testProperty: UIViewController = {
let obj: UIViewController = self
return obj
}()
#objc func actionButtonTapped() {
}
}
If you check the addTarget signature, you will see the following:
open func addTarget(_ target: Any?, action: Selector, for controlEvents: UIControl.Event)
First parameter target is Any?, so passing (Function) as target compiles fine. It even will work but can lead to weird issues, like opening keyboard will stop the button from calling the action.
You currently think that self refers to the instance.
But no, for an NSObject, it refers to partial application of this method. Actually running ViewController.self() before one has been instantiated, e.g. within the context of this closure, will crash your app, but self() used to still present itself as being available prior to Xcode 13.3.
As of now, Swift cannot cope with referring to this method without generating a warning. The warning tells you to use ViewController.self, which Swift can only interpret as a metatype, not a method. It doesn't understand what's going on, but at least it informs you that what you're doing is incorrect—the method is not actually the target.
Regardless of the warning, I don't know Objective-C well enough to tell you why a message sent to the method will trickle down to an instance of the related type. But don't do it.

How to disable reopening documents when app started via launchIsDefaultUserInfoKey

On macOS unlike iOS, it appears if you want to disable reopening documents at launch, you need to rely on the application delegate notifications vs the newer methods - with an options argument, like on iOS:
applicationWillFinishLaunching(_:), here you want to instantiate your sub-classed document controller
// We need our own to reopen our "document" urls
_ = DocumentController.init()
applicationDidFinishLaunching(_:), here you want to inspect the supplied userInfo
if let info = note.userInfo{
if let launchURL = info[NSApplication.launchIsDefaultUserInfoKey] as? String {
Swift.print("launchIsDefaultUserInfoKey: notif \(launchURL)")
disableDocumentReOpening = launchURL.boolValue
}
if let notif = info[NSApplication.launchUserNotificationUserInfoKey] as? String {
Swift.print("applicationDidFinishLaunching: notif \(notif)")
disableDocumentReOpening = true
}
}
so when my document controller is called to do the doc restores, it would see this flag within the app delegate: var disableDocumentReOpening = false.
func restoreWindow(withIdentifier identifier: NSUserInterfaceItemIdentifier, state: NSCoder, completionHandler: #escaping (NSWindow?, Error?) -> Void) {
if (NSApp.delegate as! AppDelegate).disableDocumentReOpening {
completionHandler(nil, NSError.init(domain: NSCocoaErrorDomain, code: NSUserCancelledError, userInfo: nil) )
}
else
{
NSDocumentController.restoreWindow(withIdentifier: identifier, state: state, completionHandler: completionHandler)
}
}
but unfortunately, I have something wrong but what? Manually launching the app in debugger, it appears the document controller restore call is ahead of the app delegate's routine to inspect the userInfo.
I had read a SO post on this, showing an objective-c code snippet, but flag was local to the controller, but didn't understand how you could have a read/write class var - as I tried. Also didn't understand its use of a class function as doc says its an instance method, but trying that as well didn't work either.
What am I missing?

why do I get "Attempted to unregister unknown __weak variable" when copying an instance variable?

I noticed this today when playing with NSOutlineView and NSTableHeaderCell, but when this specific configuration is made, an error/warning(?) is printed:
objc[2774]: Attempted to unregister unknown __weak variable at 0x1016070d0. This is probably incorrect use of objc_storeWeak() and objc_loadWeak(). Break on objc_weak_error to debug.
here's the snippet:
class Foo: NSCell {
weak var weak: NSView?
override func copy(with zone: NSZone? = nil) -> Any {
// according to NSCopying documentation:
// If a subclass inherits NSCopying from its superclass and declares
// additional instance variables, the subclass has to override copy(with:)
// to properly handle its own instance variables, invoking the superclass’s implementation first.
let copy = super.copy(with: zone) as! Foo
// this produces "Attempted to unregister unknown __weak variable"
copy.weak = self.weak
return copy
}
}
let view = NSView(frame: NSRect.zero)
let foo = Foo()
foo.weak = view
let copy = foo.copy() as! Foo
this also happens if I substitute NSCell with: NSEvent, NSImage, NSImageCell
but this doesn't happen to NSColor, NSDate, NSIndexPath
I started learning Swift without prior knowledge of Obj-C. could someone help me understand why this is? is it safe to ignore? who has the blame in this case?
This is a framework bug. It's easy to reproduce with the following crasher:
import Cocoa
class Cell: NSCell {
var contents: NSString?
override func copy(with zone: NSZone? = nil) -> Any {
let newObject = super.copy(with: zone) as! Cell
newObject.contents = contents
return newObject
}
}
func crash() {
let cell = Cell()
cell.contents = "hello world"
cell.copy() // crashes while releasing the copied object
}
crash()
When you use a weak var instead, you get the error message that you showed.
My gut feeling is that there is something in the copy implementation of NSCell (and possibly of NSEvent and NSImage) that does not handle subclassing for types that have non-trivial constructors. Accordingly, if you change let newObject = super.copy(...) with let newObject = Cell(), the crash is avoided. If your superclass's copy logic is simple enough, you should probably do that for now.
If you hit this problem, you should file a bug report separately of mine, but you can probably reuse my sample.

Determine if Finder can be safely killed

I am currently developing a utility program that requires the Finder to be restarted after some changes are made to the user's defaults.
To be on the safe side, I would like to check if the Finder is busy before calling killall Finder (via NSTask). If the Finder is copying files or otherwise busy, I would like to prevent the action and wait a little.
Is there a way to determine if the Finder is busy or if it can safely be killed, in Swift 2.3 on macOS 10.10+ ?
In case this is not possible, is there a safer way for me to refresh (restart) the Finder?
Thanks!
Thanks to #dfri 's comment, I was able to figure out a way (albeit not exactly the one presented in the linked answer) to do this.
Since observing the NSRunningApplication object for the Finder was not possible (the object was deinitialized due to termination before I could remove the observer), I ended up observing NSWorkspaceDidTerminateApplicationNotification from NSWorkspace.sharedWorkspace().notificationCenter
NSWorkspace.sharedWorkspace().notificationCenter.addObserver(self, selector: #selector(MyController.applicationWasTerminated(_:)), name: NSWorkspaceDidTerminateApplicationNotification, object: nil)
I can then remove this observer when my controller is deinitialized, and the selector looks like this :
func applicationWasTerminated(notification: NSNotification?) {
guard let notif = notification else { return }
guard let userInfo = notif.userInfo as? [String : AnyObject] else { return }
guard let identifier = userInfo["NSApplicationBundleIdentifier"] as? String else { return }
if identifier == "com.apple.finder" {
NSWorkspace.sharedWorkspace().launchAppWithBundleIdentifier("com.apple.finder", options: NSWorkspaceLaunchOptions.Default, additionalEventParamDescriptor: nil, launchIdentifier: nil)
}
}

How to use the delegates with NSKeyedUnarchiver?

I am using NSKeyedUnarchiver to unarchive an object and would like to use the delegates (NSKeyedUnarchiverDelegate), but my delegates are not called. Archiving and Unarchiving is working fine, but the Delegates (unarchiver & unarchiverDidFinish) are not called. Can someone help?
I have the following implementation:
class BlobHandler: NSObject , NSKeyedUnarchiverDelegate{
func load() -> MYOBJECTCLASS{
let data:NSData? = getBlob();
var mykeyedunarchiver:NSKeyedUnarchiver=NSKeyedUnarchiver(forReadingWithData: data!);
mykeyedunarchiver.delegate = self;
let temp=mykeyedunarchiver.decodeObjectForKey("rootobject")
// No delegates are called
if temp==nil {
blobsexists=false;
}else{
objectreturn = temp! as! MYOBJECTCLASS;
return objectreturn;
}
}
func save1(myobject:MYOBJECTCLASS){
let data = NSMutableData()
var keyedarchiver:NSKeyedArchiver=NSKeyedArchiver(forWritingWithMutableData: data);
keyedarchiver.encodeObject(maptheme, forKey: "rootobject");
let bytes = data.bytes;
let len=data.length;
saveblob(bytes);
}
The following delegates, which are also implemented in my Blobhandler, are never called:
func unarchiver(unarchiver: NSKeyedUnarchiver, cannotDecodeObjectOfClassName name: String, originalClasses classNames: [String]) -> AnyClass? {
print("I am in unarchiver !");
return nil;
}
func unarchiverDidFinish(_ unarchiver: NSKeyedUnarchiver){
print("I am in unarchiverDidFinish ! ");
}
I don't know what it was, but its working after a clean and rebuild of the project.
I notice with different cases, that the builds are not in sync sometimes. There is sometimes code, which is in XCode but it is not executed. Sounds unbelievable, but I guess its true.
XCode 7.2
I think the first function is never called since you didn't actually feed a "cannotDecodeObjectOfClassName" at all, since you only did try to unarchive previously archived data. You can try this method(or something requires a class name) to validate your solution(feed a class doesn't conform NSCoding):
unarchiver.decodeObjectOfClass(cls: NSCoding.Protocol, forKey: String)
The second one is a little bit tricky. I've tried this method in a similar situation and it turned out that unarchiverDidFinish only get called when a complete unarchiving job is done and probably before it's destroyed. For example, I had a NSCoding class and the convenience initiator is like
required convenience init?(coder aDecoder: NSCoder) {
let unarchiver = aDecoder as! NSKeyedUnarchiver
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
unarchiver.delegate = appDelegate.uad
let name = unarchiver.decodeObjectForKey(PropertyKey.nameKey) as! String
print(321)
self.init(name: name, photo: photo, rating: rating)
}
uad is an instance of class:
class UAD:NSObject, NSKeyedUnarchiverDelegate {
func unarchiverDidFinish(unarchiver: NSKeyedUnarchiver) {
print(123)
}
}
And in the view controller the loading process is like
func load() -> [User]? {
print(1)
let ret = NSKeyedUnarchiver.unarchiveObjectWithFile(ArchiveURL.path!) as? [User]
print(2)
return ret
}
And the output is like:
1
321
321
321
321
321
123
2
After finishing loading a group of users, the unarchiverDidFinish finally got called once. Notice that this is a class function and an anonymous instance is created to finish this sentence:
NSKeyedUnarchiver.unarchiveObjectWithFile(ArchiveURL.path!) as? [User]
So I really believe that this function only get called before it is destroyed or a group of call back functions is finished.
I am not quite sure if this is the case for you. You may try to make your unarchiver object global and destroy it after your loading is done to see whether this function is called.
Correct me if anything not right.
To make either unarchiverWillFinish: and unarchiverDidFinish: be called properly, we have to invoke finishDecoding when finished decoding.
Once you have the configured decoder object, to decode an object or data item, use the decodeObjectForKey: method. When finished decoding a keyed archive, you should invoke finishDecoding before releasing the unarchiver.
We notify the delegate of the instance of NSKeyedUnarchiver and perform any final operations on the archive through invoking this method. And once this method is invoked, according to Apple's official documentation, our unarchiver cannot decode any further values. We would get following message if we continue to perform any decoding operation after invoked finishDecoding:
*** -[NSKeyedUnarchiver decodeObjectForKey:]: unarchive already finished, cannot decode anything more
It also makes sense for encoding counterparts.