├── .gitignore ├── Package.swift ├── README.md ├── Sources └── SwiftUIWindow │ ├── HostingView.swift │ ├── SwiftUIWindow.swift │ └── WindowModifier.swift ├── Tests └── SwiftUIWindowTests │ └── SwiftUIWindowTests.swift └── demo.gif /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftUIWindow", 8 | platforms: [.macOS(.v10_15)], 9 | products: [ 10 | // Products define the executables and libraries a package produces, and make them visible to other packages. 11 | .library( 12 | name: "SwiftUIWindow", 13 | targets: ["SwiftUIWindow"]), 14 | ], 15 | dependencies: [ 16 | // Dependencies declare other packages that this package depends on. 17 | // .package(url: /* package url */, from: "1.0.0"), 18 | ], 19 | targets: [ 20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 21 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 22 | .target( 23 | name: "SwiftUIWindow", 24 | dependencies: []), 25 | .testTarget( 26 | name: "SwiftUIWindowTests", 27 | dependencies: ["SwiftUIWindow"]), 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftUIWindow 2 | Open a new macOS (10.12+) window with SwiftUI contents. Note: macOS Ventura (fall 2022) has [new ways of creating windows from SwiftUI](https://developer.apple.com/documentation/swiftui/window). 3 | 4 | # Installing 5 | Paste this repo's URL into your Package Dependencies 6 | 7 | # Demo 8 | ![Creating a SwiftUI image](/demo.gif) 9 | 10 | 11 | # Usage 12 | ``` 13 | import SwiftUIWindow 14 | ``` 15 | 16 | ## Simple standard window 17 | ```swift 18 | SwiftUIWindow.open { _ in 19 | Text("Hello new window") 20 | .frame(width: 400, height: 200) // window size 21 | } 22 | ``` 23 | 24 | ## Using modifiers 25 | This will open a floating window with no standard macOS window UI. You can move the window by dragging anywhere in the visible area. 26 | 27 | ```swift 28 | SwiftUIWindow.open { _ in 29 | VStack { 30 | Text("Hello") 31 | Button("Click me") { print("clicked") } 32 | } 33 | .frame(minWidth: 400, maxWidth: .infinity, minHeight: 200, maxHeight: .infinity) 34 | } 35 | .style(.borderless) 36 | .clickable(true) 37 | .mouseMovesWindow(true) 38 | .transparentBackground(true) 39 | .alwaysOnTop(true) 40 | ``` 41 | -------------------------------------------------------------------------------- /Sources/SwiftUIWindow/HostingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HostingView.swift 3 | // BigArea 4 | // 5 | // Created by Morten Just on 3/8/22. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | class HostingView : NSHostingView where Content : View { 12 | 13 | var mouseMovesWindow = false 14 | 15 | override public func mouseDown(with event: NSEvent) { 16 | guard mouseMovesWindow else { return } 17 | window?.performDrag(with: event) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/SwiftUIWindow/SwiftUIWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUIWindow.swift 3 | // BigArea 4 | // 5 | // Created by Morten Just on 3/8/22. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | import SwiftUI 11 | 12 | 13 | 14 | public class SwiftUIWindow : NSWindow where Content: View { 15 | 16 | var windowModifier : WindowModifier! 17 | var hostingView : HostingView! 18 | 19 | public init(@ViewBuilder content: (NSWindow) -> Content) { 20 | 21 | super.init(contentRect: .zero, styleMask: [.titled, .closable, .miniaturizable, .resizable], backing: .buffered, defer: true) 22 | 23 | hostingView = HostingView(rootView: content(self)) 24 | 25 | hostingView.isFlipped = false 26 | 27 | contentView = hostingView 28 | 29 | windowModifier = WindowModifier(window: self) 30 | } 31 | } 32 | 33 | 34 | extension SwiftUIWindow { 35 | 36 | @discardableResult 37 | public static func open(@ViewBuilder content: (NSWindow) -> Content) -> WindowModifier { 38 | 39 | 40 | 41 | let window = SwiftUIWindow(content: content) 42 | let wc = NSWindowController(window: window) 43 | 44 | 45 | 46 | print("showing window") 47 | wc.showWindow(nil) 48 | 49 | 50 | 51 | window.center() 52 | 53 | 54 | return window.windowModifier 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/SwiftUIWindow/WindowModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WindowModifier.swift 3 | // BigArea 4 | // 5 | // Created by Morten Just on 3/8/22. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | import SwiftUI 11 | 12 | public struct WindowModifier where Content: View { 13 | public let window : SwiftUIWindow 14 | 15 | 16 | @discardableResult 17 | public func style(_ style : NSWindow.StyleMask) -> WindowModifier { 18 | window.styleMask = style 19 | return self 20 | } 21 | 22 | @discardableResult 23 | public func position(_ position : NSPoint) -> WindowModifier { 24 | window.setFrameOrigin(position) 25 | return self 26 | } 27 | 28 | @discardableResult 29 | public func position(x : CGFloat) -> WindowModifier { 30 | window.setFrameOrigin(CGPoint(x: x, y: window.frame.origin.y)) 31 | return self 32 | } 33 | 34 | @discardableResult 35 | public func position(y : CGFloat) -> WindowModifier { 36 | window.setFrameOrigin(CGPoint(x: window.frame.origin.x, y: y)) 37 | return self 38 | } 39 | 40 | @discardableResult 41 | public func position(x : CGFloat, y : CGFloat) -> WindowModifier { 42 | window.setFrameOrigin(CGPoint(x: x, y: y)) 43 | return self 44 | } 45 | 46 | @discardableResult 47 | public func alwaysOnTop(_ floating : Bool) -> WindowModifier { 48 | window.level = floating ? .floating : .normal 49 | return self 50 | } 51 | 52 | @discardableResult 53 | public func level(_ level : NSWindow.Level) -> WindowModifier { 54 | window.level = level 55 | return self 56 | } 57 | 58 | @discardableResult 59 | public func movable(_ movable : Bool) -> WindowModifier { 60 | window.isMovable = movable 61 | return self 62 | } 63 | 64 | @discardableResult 65 | public func opacity(_ opacity : CGFloat) -> WindowModifier { 66 | window.alphaValue = opacity 67 | return self 68 | } 69 | 70 | @discardableResult 71 | public func backgroundColor(_ color : NSColor) -> WindowModifier { 72 | window.backgroundColor = color 73 | return self 74 | } 75 | 76 | @discardableResult 77 | public func transparentBackground(_ transparent : Bool) -> WindowModifier { 78 | if transparent { window.backgroundColor = .clear } 79 | return self 80 | } 81 | 82 | @discardableResult 83 | public func shadow(_ shadow : Bool) -> WindowModifier { 84 | window.hasShadow = shadow 85 | return self 86 | } 87 | 88 | 89 | @discardableResult 90 | public func closeOnEscape(_ close: Bool) -> WindowModifier { 91 | 92 | func localPress(_ event: NSEvent) -> NSEvent? { 93 | press(event); return event 94 | } 95 | 96 | func press(_ event : NSEvent) { 97 | if event.keyCode == 53 { 98 | window.close() 99 | } 100 | } 101 | 102 | NSEvent.addGlobalMonitorForEvents(matching: .keyDown, handler: press(_:)) 103 | NSEvent.addLocalMonitorForEvents(matching: .keyDown, handler: localPress(_:)) 104 | 105 | return self 106 | } 107 | 108 | 109 | @discardableResult 110 | public func onKeyPresses(modifiers: NSEvent.ModifierFlags?, specialKeys: NSEvent.SpecialKey?, keyCode: UInt16, perform: @escaping (NSWindow) -> Void) -> WindowModifier { 111 | 112 | func localPress(_ event: NSEvent) -> NSEvent? { 113 | press(event); return event 114 | } 115 | 116 | func press(_ event : NSEvent) { 117 | 118 | let hasModifiers = event.modifierFlags == modifiers 119 | let hasSpecials = event.specialKey == event.specialKey 120 | let hasKeycode = event.keyCode == keyCode 121 | 122 | if hasModifiers && hasSpecials && hasKeycode { 123 | NSEvent.removeMonitor(self) 124 | perform(window) 125 | } 126 | } 127 | 128 | NSEvent.addGlobalMonitorForEvents(matching: .keyDown, handler: press(_:)) 129 | NSEvent.addLocalMonitorForEvents(matching: .keyDown, handler: localPress(_:)) 130 | 131 | return self 132 | } 133 | 134 | @discardableResult 135 | public func clickable(_ clickable : Bool) -> WindowModifier { 136 | window.ignoresMouseEvents = !clickable 137 | return self 138 | } 139 | 140 | @discardableResult 141 | public func aspect(_ width : CGFloat, to height: CGFloat) -> WindowModifier { 142 | window.aspectRatio = NSSize(width: width, height: height) 143 | return self 144 | } 145 | 146 | @discardableResult 147 | public func mouseMovesWindow(_ moves : Bool) -> WindowModifier { 148 | window.hostingView.mouseMovesWindow = moves 149 | return self 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /Tests/SwiftUIWindowTests/SwiftUIWindowTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftUIWindow 3 | 4 | final class SwiftUIWindowTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | XCTAssertEqual(SwiftUIWindow().text, "Hello, World!") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mortenjust/SwiftUIWindow/0e2b92c2b2a3df817c3c4c58a03b9fc701457ac8/demo.gif --------------------------------------------------------------------------------