Skip to content

Instantly share code, notes, and snippets.

@steipete
Last active June 1, 2023 18:24
Show Gist options
  • Star 40 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save steipete/30c33740bf0ebc34a0da897cba52fefe to your computer and use it in GitHub Desktop.
Save steipete/30c33740bf0ebc34a0da897cba52fefe to your computer and use it in GitHub Desktop.
Mac Catalyst: Get the NSWindow from a UIWindow (Updated for macOS 11 Big Sur, also works with Catalina)
// Don't forget to prefix your category!
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UIWindow (PSPDFAdditions)
#if TARGET_OS_UIKITFORMAC
/**
Finds the NSWindow hosting the UIWindow.
@note This is a hack. Iterates over all windows to find match. Might fail.
*/
@property (nonatomic, readonly, nullable) id nsWindow;
#endif
@end
NS_ASSUME_NONNULL_END
#import "UIWindow+PSPDFAdditions.h"
@implementation UIWindow (PSPDFAdditions)
#if TARGET_OS_UIKITFORMAC
- (nullable NSObject *)nsWindow {
id delegate = [[NSClassFromString(@"NSApplication") sharedApplication] delegate];
const SEL hostWinSEL = NSSelectorFromString([NSString stringWithFormat:@"_%@Window%@Window:", @"host", @"ForUI"]);
@try {
// There's also hostWindowForUIWindow 🤷‍♂️
PSPDF_SILENCE_CALL_TO_UNKNOWN_SELECTOR(id nsWindow = [delegate performSelector:hostWinSEL withObject:self];)
// macOS 11.0 changed this to return an UINSWindowProxy
let attachedWin = NSSelectorFromString([NSString stringWithFormat:@"%@%@", @"attached", @"Window"]);
if ([nsWindow respondsToSelector:attachedWin]) {
nsWindow = [nsWindow valueForKey:NSStringFromSelector(attachedWin)];
}
return nsWindow;
} @catch (...) {
NSLog(@"Failed to get NSWindow for %@.", self);
}
return nil;
}
#endif
@end
@joelk
Copy link

joelk commented Nov 9, 2019

Here's a Swift version if anyone wants it:

func nsWindow(from window: UIWindow) -> AnyObject? {
    guard let nsWindows = NSClassFromString("NSApplication")?.value(forKeyPath: "sharedApplication.windows") as? [AnyObject] else { return nil }
    for nsWindow in nsWindows {
        let uiWindows = nsWindow.value(forKeyPath: "uiWindows") as? [UIWindow] ?? []
        if uiWindows.contains(window) { return nsWindow }
    }
    return nil
}

@hfabisiak
Copy link

hfabisiak commented Nov 14, 2019

@joelk Can you provide an example of the usage? It keeps returning an empty array

@nserror
Copy link

nserror commented Nov 18, 2019

Both objective-c and swift attempts return nil for me in catalyst. It gets the NSApplication just fine but the windows property is nil. Does this no longer work or is there a trick to get it working?

@steipete
Copy link
Author

I've switched to a different approach a while back - see the updated code. This is also faster, however it does use private API. Be careful.

@steipete
Copy link
Author

The macro does the folllowing:

#define PSPDF_SILENCE_CALL_TO_UNKNOWN_SELECTOR(expression) _Pragma("clang diagnostic push") _Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"") expression _Pragma("clang diagnostic pop")

@nserror
Copy link

nserror commented Nov 25, 2019

I've switched to a different approach a while back - see the updated code. This is also faster, however it does use private API. Be careful.

This works fantastic. Much appreciated!

@LeoNatan
Copy link

Please note that the host window is unavailable yet in scene:willConnectToSession:options:, so you need to dispatch_async(dispatch_get_main_queue(), ^{ });.

@polymerchm
Copy link

polymerchm commented Dec 3, 2019

Works, but returns a UINSWindow type rather than a NSWindow. YOU cannot cast it as UIWIndow nor as an NSWindow. What do you do with it? Trying to change its size and position.

@steipete
Copy link
Author

steipete commented Dec 4, 2019

UINSWindow is a subclass of NSWindow. You can't cast NSWindow in an UIKit context, but you can call things on it. We use e.g. NSWindowDidBecomeKeyNotification and then check for the window to update the selection color.

@mhdhejazi
Copy link

This is a cleaner code using Dynamic:

extension UIWindow {
    var nsWindow: NSObject? {
        Dynamic.NSApplication.sharedApplication.delegate.hostWindowForUIWindow(self)
    }
}

And this is the first approach:

extension UIWindow {
    var nsWindow: Any? {
        let windows = Dynamic.NSApplication.sharedApplication.windows.asArray
        return windows?.first { Dynamic($0).uiWindows.containsObject(self) == true }
    }
}

@polymerchm
Copy link

polymerchm commented May 23, 2020

This works great. just need to be sure to let the UI settle down with delays before you try to access the window. Using these to lock the aspect ratio in a MacCatalyst app:

delay(2) {
    cover.removeFromSuperview()
    #if targetEnvironment(macCatalyst)
    let ns = window.nsWindow
    let frame = ns?.value(forKey: "frame")
    let size = (frame as! CGRect).size
    ns!.setValue(CGSize(1.0, size.height/size.width), forKey: "aspectRatio")
    #endif
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment