上一节我们已经学习了如何在SwiftUI 中操作窗口及传值,在我们的实际项目中,我们还需要对窗口的样式进行设置,比如无边框窗口设置,所以在这一节中我们主要重点来学习如何对窗口的相关设置。
基于 AppKit App Delegate 模式下如何设置窗口样式。
上节已经讲到,窗口的初始化定义一般都在 AppDelegate.swift 这个文件中完成。所以相关窗口的操作基本也在这个文件中修改。
第1种 取消标题栏
启动页面 AppDelegate.swift 代码如下:
import Cocoa
import SwiftUI
@main
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
let contentView = ContentView()
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled,.closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
//在styleMask配置中移除 .titled 或者直接用代码移除 .titled
window.styleMask.remove(.titled)
window.isReleasedWhenClosed = false
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
}
同时我们也可以 直接在 styleMask 设置中移除 .titled 即可
styleMask: [.closable, .miniaturizable, .resizable, .fullSizeContentView],
去掉标题后,关闭按钮 和 拖动窗口 就需要自己来实现了。
如果要实现拖动窗口,有一种比较简单的办法,就是 设置窗口背景区可以支持拖动。
代码如下:
func applicationDidFinishLaunching(_ aNotification: Notification) {
let contentView = ContentView()
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled,.closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
//在styleMask配置中移除 .titled 或者直接用代码移除 .titled
window.styleMask.remove(.titled)
window.isMovableByWindowBackground=true //设置背景可以拖动窗口
window.isReleasedWhenClosed = false
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
}
依据上面的代码,我们来模拟拖动区域代码。
启动页面 AppDelegate.swift 我们添加两个方法,enableMove disableMove 用于开启和关闭拖动。
@main
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
let contentView = ContentView()
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled,.closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
//在styleMask配置中移除 .titled 或者直接用代码移除 .titled
window.styleMask.remove(.titled)
window.isReleasedWhenClosed = false
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
}
@objc func enableMove() {
print("enableMove")
window.isMovableByWindowBackground=true;
}
@objc func disableMove() {
print("disableMove")
window.isMovableByWindowBackground=false;
}
}
接着我们在 视图页面 ContentView.swift 添加一个 HStack 用作自己定义的拖动区域 并添加 onHover 事件,同时添加一个关闭按钮
struct ContentView: View {
var body: some View {
GeometryReader { proxy in
VStack{
HStack(spacing: 10){
Button("X"){
//添加关闭按钮
print("close")
//隐藏了标题以后NSApp.keyWindow 没有被激活(我个人也不是很清楚为啥没有被激活),所以我们只能遍历窗口找到并结束
NSApp.windows.forEach { (win) in
if win.frameAutosaveName=="Main Window"{
win.close()
}
}
}
}
.frame(width: proxy.size.width, height: 50, alignment: .center).background(Color.blue)
.onHover(perform: { hovering in
if(hovering){
//当鼠标覆盖区域时开启背景拖动
NSApp.sendAction(#selector(AppDelegate.enableMove), to: nil, from: nil)
}else{
//鼠标移开关闭背景拖动
NSApp.sendAction(#selector(AppDelegate.disableMove), to: nil, from: nil)
}
})
Spacer()
Text("Hello, World!")
.frame(maxWidth: .infinity, maxHeight: .infinity)
Spacer()
}
}
}
}
虽然实现了基本的功能,但是这样使用窗口可能不是最佳办法,接下来我们使用另外一种方法来实现类似的功能。
后来发现 以上的实现存在一个问题,当窗口失去焦点以后,拖动没有生效。
后修改为以下代码:
创建 TopbarView.swift 文件:
import SwiftUI
extension View {
func withTopbarView() -> some View {
self.background(TopbarView())
}
}
class NSTopbarView :NSView{
public override func mouseDown(with event: NSEvent) {
self.window!.performDrag(with: event)
super.mouseDown(with: event)
}
}
struct TopbarView :NSViewRepresentable{
func makeNSView(context: Context) -> NSTopbarView {
return NSTopbarView()
}
func updateNSView(_ nsView: NSTopbarView, context: Context) {}
}
启动页面 AppDelegate.swift 改回最开始的样子
@main
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
let contentView = ContentView().frame(width: 480, height: 300, alignment: .center)
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.isReleasedWhenClosed = false
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
}
}
在需要的元素上添加修饰
struct ContentView: View {
var body: some View {
GeometryReader { proxy in
VStack{
HStack(spacing: 10){
Spacer()
Button("X"){
//隐藏了标题以后NSApp.keyWindow 没有被激活(我个人也不是很清楚为啥没有被激活),所以我们只能遍历窗口找到并结束
NSApp.windows.forEach { (win) in
if win.frameAutosaveName=="Main Window"{
win.close()
}
}
}
Spacer().frame(width: 10, height:50, alignment: .trailing)
}
.frame(width: proxy.size.width, height: 50, alignment: .center)
//添加修饰 支持拖动窗口。
.withTopbarView()
.background(Color.blue)
Spacer()
Text("Hello, World!").frame(maxWidth: .infinity, maxHeight: .infinity)
Spacer()
}
}
}
}
最终效果:
第2种 隐藏标题栏,并设置为透明,
这样可以保留标题的相关信息和保留窗口拖动的功能。
启动页面 AppDelegate.swift 代码如下
@main
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
let contentView = ContentView()
.background(Color.white)
.edgesIgnoringSafeArea(.top) //取消顶部计算
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled,.closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.titlebarAppearsTransparent = true //设置标题栏显示为透明
window.titleVisibility = .hidden //设置标题不显示
window.isReleasedWhenClosed = false
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
//另隐藏按钮的设置
window!.standardWindowButton(NSWindow.ButtonType.zoomButton)!.isHidden = true //隐藏放大按钮
window!.standardWindowButton(NSWindow.ButtonType.miniaturizeButton)!.isHidden = true //隐藏缩小按钮
}
}
窗口效果:
1 毛玻璃背景
要制作毛玻璃效果,需要先创建一个视图结构 NSViewRepresentable
创建文件 VisualEffectView.swift
import SwiftUI
struct VisualEffectView: NSViewRepresentable
{
let material: NSVisualEffectView.Material
let blendingMode: NSVisualEffectView.BlendingMode
func makeNSView(context: Context) -> NSVisualEffectView
{
let visualEffectView = NSVisualEffectView()
visualEffectView.material = material
visualEffectView.blendingMode = blendingMode
visualEffectView.state = NSVisualEffectView.State.active
visualEffectView.layer?.cornerRadius=0
return visualEffectView
}
func updateNSView(_ visualEffectView: NSVisualEffectView, context: Context)
{
visualEffectView.material = material
visualEffectView.blendingMode = blendingMode
}
}
启动页面 AppDelegate.swift 还需要设置窗口背景透明
@main
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
let contentView = ContentView()
//设置视图背景效果
.background(VisualEffectView(
material: NSVisualEffectView.Material.titlebar,
blendingMode: NSVisualEffectView.BlendingMode.withinWindow))
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled,.closable, .miniaturizable, .resizable,.fullSizeContentView],
backing: .buffered, defer: false)
window.isReleasedWhenClosed = false
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView)
//设置窗口背景透明
//window.contentView?.wantsLayer=true
//设置窗口背景透明
window.backgroundColor = .clear
window.makeKeyAndOrderFront(nil)
}
}
效果2:
代码:
let contentView = ContentView()
.background(VisualEffectView(
material: NSVisualEffectView.Material.fullScreenUI,
blendingMode: NSVisualEffectView.BlendingMode.withinWindow))
可尝试修改不同的 material:参数设置不同的效果。
另记:
在某些情况下 窗口没有激活,keyWindow 可能会为nil 使用下面的方法可获得对应的窗口
获得当前视图所属 window 窗口中的另一种方法:
添加文件 HostingWindowFinder.swift
import SwiftUI
//扩展 View 视图的一个设置
extension View {
func withHostingWindow(_ callback: @escaping (NSWindow?) -> Void) -> some View {
self.background(HostingWindowFinder(callback: callback))
}
}
//创建一个视同,用于获取窗口
struct HostingWindowFinder: NSViewRepresentable {
var callback: (NSWindow?) -> ()
func makeNSView(context: Context) -> NSView {
let view = NSView()
DispatchQueue.main.async { [weak view] in
self.callback(view?.window)
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
}
}
在页面中使用:
struct ContentView: View {
//设置一个窗口变量,用于存储窗口
@State var window:NSWindow!
var body: some View {
GeometryReader { proxy in
VStack{
Text("Hello, World!").frame(maxWidth: .infinity, maxHeight: 50)
Button("关闭窗口"){
window.close()
}
Spacer()
}
}.withHostingWindow { (win) in
//设置窗口变量值
window = win
}
}
}