Disable NSWindow zoom button

My Catalyst App was rejected because it does not "look nice" in Mac's full screen mode. And that is true, it's designed as a windowed App. My first thought is to disable the green zoom button, but AFAIK I need a pointer to the NSWindow object to do that, and that's not available.


Any suggestions on what to do here? I do not mind if the green button toggles the App between its max size and the last user chosen size, say, but it's not proper for the App to enter full screen mode.


Thanks,

Accepted Reply

I had the same rejection. I was subsequently able to alter the zoom functionality (or disable the green maximise button, depending on what you want to do).


I haven't submitted this yet, so can't vouch for it passing app review, but here is my solution:


1. Create a new target for your app. macOS -> Bundle. Use Objective C. Call it something like HelperBundle


2. In your main target, go to General tab, add the new bundle under "Frameworks, Libraries and Embedded Contact"


3. In your bundle, create a single class, say HelperApp:


HelperApp.h:


#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface HelperApp : NSObject

+ (void) disableMaximiseButton;

@end

NS_ASSUME_NONNULL_END


HelperApp.m:


#import "HelperApp.h"

@implementation HelperApp

+ (void) disableMaximiseButton
{
    for (NSWindow *window in [[NSApplication sharedApplication] windows]) {
       
        [window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenAuxiliary|NSWindowCollectionBehaviorFullScreenNone|NSWindowCollectionBehaviorFullScreenDisallowsTiling];
       
        NSButton *button = [window standardWindowButton:NSWindowZoomButton];
        [button setEnabled: NO];
    }
}

@end

4. Now you need to call it from your main app. I made a method something like this. Sorry it’s in Obj-C:


#if TARGET_OS_MACCATALYST
- (void) catalystDisableMaximiseButton
{
    NSString *bundlePath = [NSBundle.mainBundle.builtInPlugInsPath stringByAppendingString:@"/HelperBundle.bundle"];
    NSBundle *bundle = [[NSBundle alloc] initWithPath:bundlePath];
    [bundle load];

    NSObject *object = (NSObject *) NSClassFromString(@"HelperApp");
    [object performSelector:NSSelectorFromString(@"disableMaximiseButton")];
}
#endif


5. Now I call it. I could only get it working by calling catalystDisableMaximiseButton in the main view controller’s “viewDidAppear” method

Replies

I had the same rejection. I was subsequently able to alter the zoom functionality (or disable the green maximise button, depending on what you want to do).


I haven't submitted this yet, so can't vouch for it passing app review, but here is my solution:


1. Create a new target for your app. macOS -> Bundle. Use Objective C. Call it something like HelperBundle


2. In your main target, go to General tab, add the new bundle under "Frameworks, Libraries and Embedded Contact"


3. In your bundle, create a single class, say HelperApp:


HelperApp.h:


#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface HelperApp : NSObject

+ (void) disableMaximiseButton;

@end

NS_ASSUME_NONNULL_END


HelperApp.m:


#import "HelperApp.h"

@implementation HelperApp

+ (void) disableMaximiseButton
{
    for (NSWindow *window in [[NSApplication sharedApplication] windows]) {
       
        [window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenAuxiliary|NSWindowCollectionBehaviorFullScreenNone|NSWindowCollectionBehaviorFullScreenDisallowsTiling];
       
        NSButton *button = [window standardWindowButton:NSWindowZoomButton];
        [button setEnabled: NO];
    }
}

@end

4. Now you need to call it from your main app. I made a method something like this. Sorry it’s in Obj-C:


#if TARGET_OS_MACCATALYST
- (void) catalystDisableMaximiseButton
{
    NSString *bundlePath = [NSBundle.mainBundle.builtInPlugInsPath stringByAppendingString:@"/HelperBundle.bundle"];
    NSBundle *bundle = [[NSBundle alloc] initWithPath:bundlePath];
    [bundle load];

    NSObject *object = (NSObject *) NSClassFromString(@"HelperApp");
    [object performSelector:NSSelectorFromString(@"disableMaximiseButton")];
}
#endif


5. Now I call it. I could only get it working by calling catalystDisableMaximiseButton in the main view controller’s “viewDidAppear” method

Awesome solution! I was able to make the zoom button disable with your solution.


This is my Swift version:


I created a MacHelperBundle, created a Helper.swift:

import AppKit

public class Helper {
  /// Disable the zoom button for window.
  /// - Parameter window: The window to disable.
  @objc static func disableZoomButton(for window: NSWindow) {
    window.standardWindowButton(.zoomButton)?.isEnabled = false
  }
}


In the app target

  func scene(_: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) {
    #if targetEnvironment(macCatalyst)

    // ...

    DispatchQueue.main.async { [weak self] in
      guard let builtInPlugInsPath = Bundle.main.builtInPlugInsPath else {
        return
      }
      let bundlePath = builtInPlugInsPath + "/MacHelperBundle.bundle"
      guard let bundle = Bundle(path: bundlePath) else {
        return
      }
      bundle.load()

      guard let window = self?.window?.nsWindow else {
        return
      }

      let macHelper = bundle.classNamed("MacHelperBundle.Helper") as AnyObject
      _ = macHelper.perform(NSSelectorFromString("disableZoomButtonFor:"), with: window)
    }

    #endif

    // ...
  }

extension UIWindow {

  /// Get the NSWindow from self.
  var nsWindow: 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(self) { return nsWindow }
    }
    return nil
  }
}

This is the technique I have settled on. Once you have this macOS bundle loaded you have complete access to, among everything else, NSWindow. Since you can make yourself an NSWindowDelegate you can make methods calls from UIKit to AppKit methods and handle delegate responses. You can respond to Green Button clicks, and even make that button a Zoom button rather than the default Fullscreen button.


And that is what I have done: turned the button into Zoom so I can toggle between two window sizes.


[self.window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenAuxiliary | NSWindowCollectionBehaviorFullScreenNone];


Now these window delegate methods come into action:


- (NSSize)windowWillResize:(NSWindow *) window toSize:(NSSize)newSize;

- (BOOL)windowShouldZoom:(NSWindow *)window toFrame:(NSRect)newFrame;

- (NSRect)windowWillUseStandardFrame:(NSWindow *)window defaultFrame:(NSRect)newFrame;


I also noticed that I could not make a window very small using the scene’s sizeRestriction:


self.windowScene.sizeRestrictions.minimumSize = kMacCatMinimumWindowSize;


But after setting the window’s minSize you can, mine is now 60x60:


self.window.minSize = kMacCatMinimumWindowSize;


BTW this is how I instantiate the glue class (ignore line #3, cannot seem to delete that line in the web page editor):


    NSBundle *catalystBundle = [NSBundle bundleWithPath:[[[NSBundle mainBundle] builtInPlugInsPath] stringByAppendingPathComponent:@"AppKitGlue.bundle"]];
    BOOL catalystBundleIsLoaded = [catalystBundle load];
    macOS = [macOS stringByAppendingFormat:@"/%d", catalystBundleIsLoaded];
    self.appKitGlue = [[[catalystBundle principalClass] alloc] init];

> I haven't submitted this yet, so can't vouch for it passing app review, but here is my solution:


Have you submitted the App and did it pass review? Now you have me worried that I've wasted a lot of time on this. Which would be a shame, the App's window behavior is super cool and very Mac-like.