MacOS Catalyst: how to allow double click on UIView to zoom window?

By default, in MacOS, you can double click the titlebar of a window to zoom it (resize to fit the screen - different from maximize). Double clicking it again brings it back to the previous size.

This works fine on my Catalyst app too. However I need to hide the titlebar and need to give my own custom UIView in that titlebar area the double click behavior. I am able to hide it using this:
Code Block
#if os(OSX) || os(macOS) || targetEnvironment(macCatalyst)
UIApplication.shared.connectedScenes.forEach({
if let titlebar = ($0 as? UIWindowScene)?.titlebar {
titlebar.titleVisibility = .hidden
titlebar.toolbar = nil
}
})
#endif

Is there a method which lets me toggle the window zoom?
Answered by pranapps in 670589022
I was able to figure this out using a workaround. UIKit/Catalyst itself doesn't provide any way to do this. But I was able to use the second method outline in this post on the link in my answer on Stackoverflow:

How to Access the AppKit API from Mac Catalyst Apps

https://stackoverflow.com/questions/67067248/macos-catalyst-how-to-allow-double-click-on-uiview-to-zoom-window/67084666#67084666

I used the second method and not the first one as the first one seems to be private API (I could be wrong) and will get rejected in App Store. The second method of using a plugin bundle and calling methods on that works well for me. This way I was able to not just perform the zoom, I was also able to perform other MacOS Appkit functionality like listening for keyboard, mouse scroll, hover detection etc.

After creating the plugin bundle, here's my code inside the plugin:

Plugin.swift:
Code Block
import Foundation
@objc(Plugin)
protocol Plugin: NSObjectProtocol {
init()
func toggleZoom()
func macOSStartupStuff()
}

MacPlugin.swift:
Code Block
import AppKit
class MacPlugin: NSObject, Plugin {
required override init() {}
func macOSStartupStuff() {
NSApplication.shared.windows.forEach({
$0.titlebarAppearsTransparent = true
$0.titleVisibility = .hidden
$0.backgroundColor = .clear
($0.contentView?.superview?.allSubviews.first(where: { String(describing: type(of: $0)).hasSuffix("TitlebarDecorationView") }))?.alphaValue = 0
})
}
func toggleZoom(){
NSApplication.shared.windows.forEach({
$0.performZoom(nil)
})
}
}
extension NSView {
var allSubviews: [NSView] {
return subviews.flatMap { [$0] + $0.allSubviews }
}
}

Then I call this from my iOS app code. This adds a transparent view at the top where double clicking calls the plugin code for toggling zoom.

NOTE that you must call this from viewDidAppear or somewhere when the windows have been initialized and presented. Otherwise it won't work.
Code Block
#if os(OSX) || os(macOS) || targetEnvironment(macCatalyst)
@objc func zoomTapped(){
plugin?.toggleZoom()
}
var pluginWasLoaded = false
lazy var plugin : Plugin? = {
pluginWasLoaded = true
if let window = (UIApplication.shared.delegate as? AppDelegate)?.window {
let transparentTitleBarForDoubleClick = UIView(frame: .zero)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(zoomTapped))
tapGesture.numberOfTapsRequired = 2
transparentTitleBarForDoubleClick.addGestureRecognizer(tapGesture)
transparentTitleBarForDoubleClick.isUserInteractionEnabled = true
transparentTitleBarForDoubleClick.backgroundColor = .clear
transparentTitleBarForDoubleClick.translatesAutoresizingMaskIntoConstraints = false
window.addSubview(transparentTitleBarForDoubleClick)
window.bringSubviewToFront(transparentTitleBarForDoubleClick)
window.addConstraints([
NSLayoutConstraint(item: transparentTitleBarForDoubleClick, attribute: .leading, relatedBy: .equal, toItem: window, attribute: .leading, multiplier: 1, constant: 0),
NSLayoutConstraint(item: transparentTitleBarForDoubleClick, attribute: .top, relatedBy: .equal, toItem: window, attribute: .top, multiplier: 1, constant: 0),
NSLayoutConstraint(item: transparentTitleBarForDoubleClick, attribute: .trailing, relatedBy: .equal, toItem: window, attribute: .trailing, multiplier: 1, constant: 0),
transparentTitleBarForDoubleClick.bottomAnchor.constraint(equalTo: window.safeTopAnchor)
])
window.layoutIfNeeded()
}
guard let bundleURL = Bundle.main.builtInPlugInsURL?.appendingPathComponent("MacPlugin.bundle") else { return nil }
guard let bundle = Bundle(url: bundleURL) else { return nil }
guard let pluginClass = bundle.classNamed("MacPlugin.MacPlugin") as? Plugin.Type else { return nil }
return pluginClass.init()
}()
#endif

Calling it from viewDidAppear:
Code Block
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
#if os(OSX) || os(macOS) || targetEnvironment(macCatalyst)
if !Singleton.shared.pluginWasLoaded {
Singleton.shared.plugin?.macOSStartupStuff()
}
#endif
}
Accepted Answer
I was able to figure this out using a workaround. UIKit/Catalyst itself doesn't provide any way to do this. But I was able to use the second method outline in this post on the link in my answer on Stackoverflow:

How to Access the AppKit API from Mac Catalyst Apps

https://stackoverflow.com/questions/67067248/macos-catalyst-how-to-allow-double-click-on-uiview-to-zoom-window/67084666#67084666

I used the second method and not the first one as the first one seems to be private API (I could be wrong) and will get rejected in App Store. The second method of using a plugin bundle and calling methods on that works well for me. This way I was able to not just perform the zoom, I was also able to perform other MacOS Appkit functionality like listening for keyboard, mouse scroll, hover detection etc.

After creating the plugin bundle, here's my code inside the plugin:

Plugin.swift:
Code Block
import Foundation
@objc(Plugin)
protocol Plugin: NSObjectProtocol {
init()
func toggleZoom()
func macOSStartupStuff()
}

MacPlugin.swift:
Code Block
import AppKit
class MacPlugin: NSObject, Plugin {
required override init() {}
func macOSStartupStuff() {
NSApplication.shared.windows.forEach({
$0.titlebarAppearsTransparent = true
$0.titleVisibility = .hidden
$0.backgroundColor = .clear
($0.contentView?.superview?.allSubviews.first(where: { String(describing: type(of: $0)).hasSuffix("TitlebarDecorationView") }))?.alphaValue = 0
})
}
func toggleZoom(){
NSApplication.shared.windows.forEach({
$0.performZoom(nil)
})
}
}
extension NSView {
var allSubviews: [NSView] {
return subviews.flatMap { [$0] + $0.allSubviews }
}
}

Then I call this from my iOS app code. This adds a transparent view at the top where double clicking calls the plugin code for toggling zoom.

NOTE that you must call this from viewDidAppear or somewhere when the windows have been initialized and presented. Otherwise it won't work.
Code Block
#if os(OSX) || os(macOS) || targetEnvironment(macCatalyst)
@objc func zoomTapped(){
plugin?.toggleZoom()
}
var pluginWasLoaded = false
lazy var plugin : Plugin? = {
pluginWasLoaded = true
if let window = (UIApplication.shared.delegate as? AppDelegate)?.window {
let transparentTitleBarForDoubleClick = UIView(frame: .zero)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(zoomTapped))
tapGesture.numberOfTapsRequired = 2
transparentTitleBarForDoubleClick.addGestureRecognizer(tapGesture)
transparentTitleBarForDoubleClick.isUserInteractionEnabled = true
transparentTitleBarForDoubleClick.backgroundColor = .clear
transparentTitleBarForDoubleClick.translatesAutoresizingMaskIntoConstraints = false
window.addSubview(transparentTitleBarForDoubleClick)
window.bringSubviewToFront(transparentTitleBarForDoubleClick)
window.addConstraints([
NSLayoutConstraint(item: transparentTitleBarForDoubleClick, attribute: .leading, relatedBy: .equal, toItem: window, attribute: .leading, multiplier: 1, constant: 0),
NSLayoutConstraint(item: transparentTitleBarForDoubleClick, attribute: .top, relatedBy: .equal, toItem: window, attribute: .top, multiplier: 1, constant: 0),
NSLayoutConstraint(item: transparentTitleBarForDoubleClick, attribute: .trailing, relatedBy: .equal, toItem: window, attribute: .trailing, multiplier: 1, constant: 0),
transparentTitleBarForDoubleClick.bottomAnchor.constraint(equalTo: window.safeTopAnchor)
])
window.layoutIfNeeded()
}
guard let bundleURL = Bundle.main.builtInPlugInsURL?.appendingPathComponent("MacPlugin.bundle") else { return nil }
guard let bundle = Bundle(url: bundleURL) else { return nil }
guard let pluginClass = bundle.classNamed("MacPlugin.MacPlugin") as? Plugin.Type else { return nil }
return pluginClass.init()
}()
#endif

Calling it from viewDidAppear:
Code Block
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
#if os(OSX) || os(macOS) || targetEnvironment(macCatalyst)
if !Singleton.shared.pluginWasLoaded {
Singleton.shared.plugin?.macOSStartupStuff()
}
#endif
}
MacOS Catalyst: how to allow double click on UIView to zoom window?
 
 
Q