I have a program that allows me to programmatically bounce a ball around a window. Everything works as desired. What I would like to be able to do is work with the window itself to do such things as remove the title, change the background color and make it non resizable. I think I do this by creating a class of type NSWindosController which I have done. I create an instance of it in the ContentView but it does not work. Is my class wrong or am I not creating an instance in the right place or is there some other problem. Thank you.
import Cocoa
class WindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
window?.styleMask.remove(.titled)
window?.styleMask.remove(.resizable)
window?.backgroundColor = .red
}
}
import SwiftUI
struct ColorShading: Identifiable {
let value: GraphicsContext.Shading
let id = UUID()
}
struct ContentView: View {
@State private var isPaused: Bool = true
@State private var ballPosition = BallPosition()
@State private var winController = WindowController()
var body: some View {
let ballColors = [
ColorShading(value: GraphicsContext.Shading.color(.orange)),
ColorShading(value: GraphicsContext.Shading.color(.red)),
ColorShading(value: GraphicsContext.Shading.color(.blue)),
ColorShading(value: GraphicsContext.Shading.color(.green)),
ColorShading(value: GraphicsContext.Shading.color(.gray)),
ColorShading(value: GraphicsContext.Shading.color(.yellow)),
ColorShading(value: GraphicsContext.Shading.color(.purple))
]
TimelineView(.animation(minimumInterval: 0.0001, paused: isPaused)) { timeline in
Canvas { context, size in
_ = timeline.date.timeIntervalSinceNow
let circleDiameter: Double = (size.width * 0.2)
let circleRadius: Double = circleDiameter / 2
if isPaused == true {
ballPosition.updateBaseData(maxX: size.width, maxY: size.height, circleRadius: circleRadius)
}
let wheelOrigin = CGPoint(x: ballPosition.xPosition, y: ballPosition.yPostion)
context.stroke(
Path { path in
path.addArc(center: wheelOrigin, radius: circleRadius, startAngle: .degrees(0), endAngle: .degrees(360), clockwise: false)
}, with: ballColors[ballPosition.randomColor].value, lineWidth: 2
) // end context stroke
ballPosition.updatePosition()
} // end canvas
.frame(width: 800, height: 800)
} // end time line view
.onAppear() {
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { timer in
isPaused = false
timer.invalidate()
} // end timer
} // end on appear
}
}
Looks like I was heading down the wrong path. I find that I need to work with NSWindow, not NSWindowController, and among other things, its style mask. I found some code on line that lets me position the window on the screen. I made some modifications to it and added a function setStyleMask which is an extension to NSWndow. It allows me to make the modifications I want. Below is the updated code. My ContentView does not change with the exception of the elimination of @State private var winController = WinController().
import SwiftUI
@main
struct BouncingWheelApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.hostingWindowPosition(
vertical: .center,
horizontal: .center,
screen: .main)
}
}
}
import SwiftUI
import AppKit
extension NSWindow {
func setStyleMask() {
titlebarAppearsTransparent = true
titleVisibility = .hidden
backgroundColor = .gray
styleMask.remove(.resizable)
}
func setPosition(_ position: Position, in screen: NSScreen?) {
setStyleMask()
guard let visibleFrame = (screen ?? self.screen)?.visibleFrame else { return }
let origin = position.value(forWindow: frame, inScreen: visibleFrame)
let myRect = CGRect(x: origin.x, y: origin.y, width: 800.0, height: 800.0)
setFrame(myRect, display: true)
} // end function set position
struct Position {
static let defaultPadding: CGFloat = 16
var vertical: Vertical
var horizontal: Horizontal
var padding = Self.defaultPadding
enum Horizontal {
case left, center, right
}
enum Vertical {
case top, center, bottom
}
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)
}
} // end structure Position
}
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
}
}
}
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
)
)
}
}