Sometimes its easier to just enable MacCatalyst on an iOS app to bring it to the Mac than to fiddle around with a dedicated Mac target or using the Mac version of SwiftUI. The drawback is that you don’t have access to AppKit but have to stick with UIKit / SwiftUI. So some quite useful features of AppKit are not available in MacCatalyst.
E.g. there is this nice AppKit construct to save and restore the window size and position between app launches:
1 |
controller.windowFrameAutosaveName = NSWindow.FrameAutosaveName("MyWindow") |
But there is no equivalent in MacCatalyst since iOS apps don’t mess around with windows. In this post I’ll show how to achieve a similar behaviour in MacCatalyst.
First of all we have to implement a SceneDelegate
for our app. That’s straightforward for UIKit but a little more complicated for SwiftUI. Take a look at this post for that:
https://www.fivestars.blog/articles/app-delegate-scene-delegate-swiftui/
It’s a good idea to remember the corresponding window scene in the delegate:
1 2 3 |
class MySceneDelegate: NSObject, UIWindowSceneDelegate, ObservableObject { var scene: UIWindowScene? // remember our window scene … |
And then we’ll implement this delegate function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
… func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { #if targetEnvironment(macCatalyst) // only for running on a Mac guard let windowScene = scene as? UIWindowScene else { return } self.scene = windowScene // remember our window scene var preferredRect: CGRect? { // is a size preference present? var rect: CGRect? // by default its not let width = UserDefaults.standard.double(forKey: MyMessages.windowSizeWidth) let height = UserDefaults.standard.double(forKey: MyMessages.windowSizeHeight) let x = UserDefaults.standard.double(forKey: MyMessages.windowOriginX) let y = UserDefaults.standard.double(forKey: MyMessages.windowOriginY) if width > 0 && height > 0 { // we do have a valid size rect = CGRect(x: x, y: y, width: width, height: height) } return rect // return the rect (or nothing) } if let rect = preferredRect { // we do have a rect let geoPrefs = UIWindowScene.GeometryPreferences.Mac(systemFrame: rect) windowScene.requestGeometryUpdate(geoPrefs) // apply it to the current scene } #endif } } |
After we’ve made sure that we do have a window scene we’ll store that scene in the above mentioned var
. In the computed property preferredRect
we’ll look for a possibly stored window rect. How that is stored we’ll see in a minute.
If that rect is available we put it in GeometryPreferences
for the Mac and request an update of the geometry. That’s it for the restore.
To store the current geometry of our window we can use any View
in our app with a GeometryReader
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
struct MyView: View { @EnvironmentObject var sceneDelegate: MySceneDelegate … var body: some View { GeometryReader { geo in … .onChange(of: geo.size) { // size changed #if targetEnvironment(macCatalyst) // are we running on a Mac? if let rect = sceneDelegate.scene?.effectiveGeometry.systemFrame { // get the system frame from the scene delegate UserDefaults.standard.set(rect.width, forKey: MyMessages.windowSizeWidth) // and set the user defaults UserDefaults.standard.set(rect.height, forKey: MyMessages.windowSizeHeight) UserDefaults.standard.set(rect.origin.x, forKey: MyMessages.windowOriginX) UserDefaults.standard.set(rect.origin.y, forKey: MyMessages.windowOriginY) } #endif } } } } |
Here we get the current system frame from our sceneDelegate
and store in the UserDefaults
. Well, that’s it. Not too difficult, is it?