Mac Catalyst on Multiple Displays: Using UIWindowSceneGeometryPreferencesMac to set a default window size causes new windows to open on the wrong display

In the WWDC 2022 video "Bring your iOS App to the Mac" there is sample code which shows the recommended way to set the default window size for a new window. According to the presenter it is considered good practice to do this in -scene:willConnectToSession:options:

CGRect systemFrame = scene.effectiveGeometry.systemFrame;
CGRect newFrame = CGRectMake(systemFrame.origin.x,
                                     systemFrame.origin.y,
                                     defaultWindowSize.width,
                                     defaultWindowSize.height);

 UIWindowSceneGeometryPreferencesMac *geometryPrefs = [[UIWindowSceneGeometryPreferencesMac alloc]initWithSystemFrame:newFrame];

        [scene requestGeometryUpdateWithPreferences:geometryPrefs errorHandler:^(NSError * _Nonnull error)

         {
             //Error
        }];

So I have a button in my UI that opens a new window that uses this window scene delegate class. I have a laptop and external display connected. Now my app's window is on the external display and when I click the button the opens the new window, the new window opens on the laptop's main display (the display physically attached). This is wrong.

Setting the requestingScene property on UISceneActivationRequestOptions makes no difference, the new window still opens on the wrong display no matter what.

If I comment out the above code that sets the initial window size... the window opens on external display as expected (but now I've lost my window size).

I can't even try setting the frame from the call site with this API:

 [application requestSceneSessionActivation:nil

                           userActivity:theActivity

                               options:activationOptions

                          errorHandler:^(NSError * _Nonnull error)

    {
    }];

}

Because there is an error handler but I don't get back the created UIWindowScene on success....

Is it really necessary to have an error handler on opening a window? An error handler on setting a window's frame? Why is everything hard to do? Passing data to a new window has to be done through these absurd NSUserActivity objects....

Like why isn't there a normal API?

windowController.window.frame = initialRect;
windowController.modelData = modelObject;
[windowController showWindow:sender];

I need an error handler to set a frame and to open a window? Am I making a network request? And if I get an error on set frame how exactly am I supposed to handle the error? Set the frame again and hope that it works?

I filed FB11885144

I usually don't do this, but I wonder if not enough developers are providing feedback so I'm going to go on a short tangent. Just to go back to my snippet of what I consider to be “normal API”:

windowController.window.frame = initialRect;
windowController.modelData = modelObject;
[windowController showWindow:sender];

This coding style has essentially been “abstracted away” in Catalyst underneath this:

NSUserActivity *newWindowActivity = [[NSUserActivity alloc]initWithActivityType:@“my.new.window.activitytype”];
//Our options to pass data to the new window are the following:
//Option 1) Write methods to convert a model object to and from one of the types NSUserActivity's userInfo accepts.
//Option 2) Make the model object conform to NSSecureCoding and convert it to and from NSData and stuff the NSData in the activity).
//Option 3) Pass a "database identifier" and query for the model object on the other end from a singleton and/or database.
//In this sample I'm using option 1 because that's the "easiest." Not always possible though.

NSArray *modelAsPlist = [modelData propertyListRepresentation];
NSDictionary *modelDictionary = @{@"NewWindowModelData":modelAsPlist}; 
[newWindowActivity addUserInfoEntriesFromDictionary:modelDictionary];

UISceneActivationRequestOptions *activationOptions = [[UISceneActivationRequestOptions alloc]init];
activationOptions.requestingScene = self.view.window.windowScene;
UIApplication *application = [UIApplication sharedApplication];

[application requestSceneSessionActivation:nil 
                              userActivity:newWindowActivity
                                   options:activationOptions
errorHandler:^(NSError * _Nonnull error)
    {
        //Whoops the system didn't give me a window. Tough luck. What kind of error handling are application developers expected to do here?
    }];

And we still haven't achieved the three lines of the "normal API" yet. The controller that needs the model object still hasn't gotten it yet. And the initial frame for the window isn't set. Now on the other end in the UIWindowSceneDelegate:

-(void)scene:(UIWindowScene*)scene
willConnectToSession:(UISceneSession*)session
     options:(UISceneConnectionOptions*)connectionOptions
{
    //connectionOptions can have more than 1 activity...we gotta dig for our model object and that's the bottom line because the System says so.
    NSUserActivity *activityToConfigureWith = nil;
    for (NSUserActivity *aActivity in connectionOptions.userActivities)
    {
        if (aActivity.activityType isEqualToString:@"my.new.window.activitytype"])
        {
            activityToConfigureWith = aActivity;
            break;
        }
    }

    if (activityToConfigureWith != nil)
    {
         NSArray *topLevelModelPlist =  activityToConfigureWith.userInfo[@"NewWindowModelData"];
         ModelData *model = [ModelData makeWithPropertyList:topLevelModelPlist];

         MyViewController *myVC = [[MyViewController alloc]initWithModel:model];

         self.window = [[UIWindow alloc]initWithWindowScene:scene];
         self.window.rootViewController = myVC;

         [self.window makeKeyAndVisible];
    }
    else
    {
      //Not found. Maybe we have a state restoration activity to configure..
      //Or maybe not. Maybe we are just on app launch. We have to sort our way through this pile of user activities just to get started here..it's kind of a mess.
    }

       CGRect systemFrame = scene.effectiveGeometry.systemFrame;
        CGRect newFrame = systemFrame;
       newFrame.size.width = DEFAULT_WIDTH;
        newFrame.size.height = DEFAULT_HEIGHT;
            
        UIWindowSceneGeometryPreferencesMac *geometryPrefs = [[UIWindowSceneGeometryPreferencesMac alloc]initWithSystemFrame:newFrame];

         [scene requestGeometryUpdateWithPreferences:geometryPrefs errorHandler:^(NSError * _Nonnull error)
         {
            //Error setting frame. Tough luck! What kind of error handling are application developers expected to do here?
        }];
}

Now in my real app a lot of this code I silo in categories because the amount of code required for such simple tasks is kind of absurd. All we did is 1) create a window 2) set its initial frame and model object and 3) show it on screen.

Not sure how this was decided on but IMHO the abstraction is 10x more complex than the internal implementation. At the very least creating and passing data to a new window shouldn't be any harder than creating a new view controller programmatically or using storyboards with -prepareForSegue: But it is a lot harder. And requires lots more boilerplate.

This doesn't include the code required to workaround the issue described in FB11885144.

If you want to make it easy to port iOS apps to the Mac maybe just expose a NSViewController subclass in the public API that embeds a UIKit view hierarchy inside of it and let developers use them in NSWindows directly? Maybe?

Mac Catalyst on Multiple Displays: Using UIWindowSceneGeometryPreferencesMac to set a default window size causes new windows to open on the wrong display
 
 
Q