Skip to content

Instantly share code, notes, and snippets.

@ABridoux
Last active April 14, 2024 12:01
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ABridoux/b935c21c7ead92033d39b357fae6366b to your computer and use it in GitHub Desktop.
Save ABridoux/b935c21c7ead92033d39b357fae6366b to your computer and use it in GitHub Desktop.
Logic to easily set a NSWindow's origin horizontally and vertically in a screen (AppKit and SwiftUI)
// Free to use
// Written by Alexis Bridoux - https://github.com/ABridoux
import AppKit
#if canImport(SwiftUI)
import SwiftUI
#endif
// MARK: Model
extension NSWindow {
struct Position {
static let defaultPadding: CGFloat = 16
var vertical: Vertical
var horizontal: Horizontal
var padding = Self.defaultPadding
}
}
extension NSWindow.Position {
enum Horizontal {
case left, center, right
}
enum Vertical {
case top, center, bottom
}
}
// MARK: Logic
extension NSWindow.Position {
func value(forWindow windowRect: CGRect, inScreen screenRect: CGRect) -> CGPoint {
let xPosition = horizontal.valueFor(
screenRange: screenRect.minX..<screenRect.maxX,
width: windowRect.width,
padding: padding
)
let yPosition = vertical.valueFor(
screenRange: screenRect.minY..<screenRect.maxY,
height: windowRect.height,
padding: padding
)
return CGPoint(x: xPosition, y: yPosition)
}
}
extension NSWindow.Position.Horizontal {
func valueFor(
screenRange: Range<CGFloat>,
width: CGFloat,
padding: CGFloat)
-> CGFloat {
switch self {
case .left: return screenRange.lowerBound + padding
case .center: return (screenRange.upperBound + screenRange.lowerBound - width) / 2
case .right: return screenRange.upperBound - width - padding
}
}
}
extension NSWindow.Position.Vertical {
func valueFor(
screenRange: Range<CGFloat>,
height: CGFloat,
padding: CGFloat)
-> CGFloat {
switch self {
case .top: return screenRange.upperBound - height - padding
case .center: return (screenRange.upperBound + screenRange.lowerBound - height) / 2
case .bottom: return screenRange.lowerBound + padding
}
}
}
// MARK: - AppKit extension
extension NSWindow {
func setPosition(_ position: Position, in screen: NSScreen?) {
guard let visibleFrame = (screen ?? self.screen)?.visibleFrame else { return }
let origin = position.value(forWindow: frame, inScreen: visibleFrame)
setFrameOrigin(origin)
}
func setPosition(
vertical: Position.Vertical,
horizontal: Position.Horizontal,
padding: CGFloat = Position.defaultPadding,
screen: NSScreen? = nil)
{
setPosition(
Position(vertical: vertical, horizontal: horizontal, padding: padding),
in: screen
)
}
}
// MARK: - SwiftUI modifier
#if canImport(SwiftUI)
/// - note: Idea from [LostMoa](https://lostmoa.com/blog/ReadingTheCurrentWindowInANewSwiftUILifecycleApp/)
struct HostingWindowFinder: NSViewRepresentable {
var callback: (NSWindow?) -> ()
func makeNSView(context: Self.Context) -> NSView {
let view = NSView()
DispatchQueue.main.async { self.callback(view.window) }
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
DispatchQueue.main.async { self.callback(nsView.window) }
}
}
private struct WindowPositionModifier: ViewModifier {
let position: NSWindow.Position
let screen: NSScreen?
func body(content: Content) -> some View {
content.background(
HostingWindowFinder {
$0?.setPosition(position, in: screen)
}
)
}
}
extension View {
func hostingWindowPosition(
vertical: NSWindow.Position.Vertical,
horizontal: NSWindow.Position.Horizontal,
padding: CGFloat = NSWindow.Position.defaultPadding,
screen: NSScreen? = nil
) -> some View {
modifier(
WindowPositionModifier(
position: NSWindow.Position(
vertical: vertical,
horizontal: horizontal,
padding: padding
),
screen: screen
)
)
}
}
#endif
@ABridoux
Copy link
Author

ABridoux commented Aug 8, 2021

Explanations

Read the article

How to use it ?

AppKit

window.setPosition(vertical: .top, horizontal: .center)
// or
window.setPosition(vertical: .bottom, horizontal: .left, padding: 20)
// or
window.setPosition(vertical: .center, horizontal: .center, screen: .main)

SwiftUI

ContentView()
    .hostingWindowPosition(
        vertical: .bottom,
        horizontal: .left,
        screen: .main
    )

@winddpan
Copy link

winddpan commented Jun 6, 2023

callback in NSView draw instead of DispatchQueue.main.async avoding window flashing.

// Free to use
// Written by Alexis Bridoux - https://github.com/ABridoux

import AppKit

#if canImport(SwiftUI)
import SwiftUI
#endif

public extension NSWindow {
    struct Position {
        var vertical: Vertical
        var horizontal: Horizontal
        var padding: CGSize
    }
}

public extension NSWindow.Position {
    enum Horizontal {
        case left, center, right
    }

    enum Vertical {
        case top, center, bottom
    }
}

// MARK: Logic

private extension NSWindow.Position {
    func value(forWindow windowRect: CGRect, inScreen screenRect: CGRect) -> CGPoint {
        let xPosition = horizontal.valueFor(
            screenRange: screenRect.minX..<screenRect.maxX,
            width: windowRect.width,
            padding: padding.width
        )

        let yPosition = vertical.valueFor(
            screenRange: screenRect.minY..<screenRect.maxY,
            height: windowRect.height,
            padding: padding.height
        )

        return CGPoint(x: xPosition, y: yPosition)
    }
}

private extension NSWindow.Position.Horizontal {
    func valueFor(
        screenRange: Range<CGFloat>,
        width: CGFloat,
        padding: CGFloat
    )
        -> CGFloat
    {
        switch self {
        case .left: return screenRange.lowerBound + padding
        case .center: return (screenRange.upperBound + screenRange.lowerBound - width) / 2
        case .right: return screenRange.upperBound - width - padding
        }
    }
}

private extension NSWindow.Position.Vertical {
    func valueFor(
        screenRange: Range<CGFloat>,
        height: CGFloat,
        padding: CGFloat
    )
        -> CGFloat
    {
        switch self {
        case .top: return screenRange.upperBound - height - padding
        case .center: return (screenRange.upperBound + screenRange.lowerBound - height) / 2
        case .bottom: return screenRange.lowerBound + padding
        }
    }
}

// MARK: - AppKit extension

public extension NSWindow {
    func setPosition(_ position: Position, in screen: NSScreen?) {
        guard let visibleFrame = (screen ?? self.screen)?.visibleFrame else { return }
        let origin = position.value(forWindow: frame, inScreen: visibleFrame)
        setFrameOrigin(origin)
    }

    func setPosition(
        vertical: Position.Vertical,
        horizontal: Position.Horizontal,
        padding: CGSize = .zero,
        screen: NSScreen? = nil
    ) {
        setPosition(
            Position(vertical: vertical, horizontal: horizontal, padding: padding),
            in: screen
        )
    }
}

// MARK: - SwiftUI modifier

#if canImport(SwiftUI)

/// - note: Idea from [LostMoa](https://lostmoa.com/blog/ReadingTheCurrentWindowInANewSwiftUILifecycleApp/)
private struct HostingWindowFinder: NSViewRepresentable {
    var callback: (NSWindow) -> ()

    func makeNSView(context: Self.Context) -> NSView {
        let view = BridgingView()
        view.callback = callback
        return view
    }

    func updateNSView(_ nsView: NSView, context: Context) {}
}

private class BridgingView: NSView {
    var callback: ((NSWindow) -> ())?

    override func draw(_ dirtyRect: NSRect) {
        super.draw(dirtyRect)

        if let window = window {
            callback?(window)
        }
    }
}

private struct WindowPositionModifier: ViewModifier {
    let position: NSWindow.Position
    let screen: NSScreen?

    func body(content: Content) -> some View {
        content.background(
            HostingWindowFinder {
                $0.setPosition(self.position, in: self.screen)
            }
        )
    }
}

public extension View {
    func hostingWindowPosition(
        vertical: NSWindow.Position.Vertical,
        horizontal: NSWindow.Position.Horizontal,
        padding: CGSize = .zero,
        screen: NSScreen? = nil
    ) -> some View {
        modifier(
            WindowPositionModifier(
                position: NSWindow.Position(
                    vertical: vertical,
                    horizontal: horizontal,
                    padding: padding
                ),
                screen: screen
            )
        )
    }
}
#endif

@ABridoux
Copy link
Author

ABridoux commented Jun 7, 2023

@winddpan I'll try that and update the Gist. Thanks!

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