当前位置:首页 > 操作系统 > MacOs

学习使用SwiftUI开发MacOS 应用- 第一节 如何创建SwiftUI 应用并实现窗口交互

在这一节里,我们不和其他教程一样细讲每个实现原理,从我们大多数应用中经常碰到的窗口操作去实现,比如 如何在SwiftUI 中实现一个登陆窗口,并且当成功登陆后关闭登陆窗口并打开主窗口,以及了解如何设置窗口相关属性。

第一步  创建项目
我们先学习如何创建SwiftUI项目,和在项目中选择何种方式去创建SwiftUI 项目,即两种方式创建SwiftUI项目的区别和在实际项目中如何使用他们。
首先我们打开 Xcode 创建项目,在项目中我们选择 macOS 类型的项目,并选择 App  点击 next
学习使用SwiftUI开发MacOS 应用- 第一节 如何创建SwiftUI 应用并实现窗口交互 - 文章图片
进入到项目参数选择界面
学习使用SwiftUI开发MacOS 应用- 第一节 如何创建SwiftUI 应用并实现窗口交互 - 文章图片
先填写项目名称  我这里填写  MyApp 在选择 Life Cycle  为 AppKit App Delegate 项目类型,这里还有一个 SwiftUI App
学习使用SwiftUI开发MacOS 应用- 第一节 如何创建SwiftUI 应用并实现窗口交互 - 文章图片

我们先看看  AppKit App Delegate 的代码目录结构 ,这里是我们比较关心的文件
AppDelegate.swift 文件中 主要有启动项目的相关设置代码。项目的启动是从这里开始的。
ContentView.swift 这个是我们窗口的内容视图页面,也是我们主要的开发编辑视图的文件。
Assets.xcassets 这个是我们项目用到的资源文件目录,以及相应的资源设置目录
Main.storyboard 这个是我们顶部菜单,及其他菜单,等相关设置,在 info.plist 指定了这个文件,所以不要删除。
info.plist 是苹果的一些配置信息文件。
MyApp.entitlements 文件是一些权限配置,服务调用配置,证书配置等相关的配置文件。

第二步 创建登录窗口并打开主窗口

我们来了解一下项目文件,及制作我们的登录窗口,并从登录窗口中打开我们的主窗口。
我们先打开 AppDelegate.swift 文件。

<code>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 需要标题,.closable 可以关闭窗口,.miniaturizable 可以最小化,.resizable 可改变窗口大小,.fullSizeContentView 可全屏当前视图

        //设置窗口如果关闭后是否销毁窗口,这里设置为false 即关闭窗口不销毁窗口,可以被下次makeKeyAndOrderFront 再次显示。
        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
    }


}
</code>

这是系统创建的默认启动页面代码。可以从我的注释中去了解代码相关的逻辑。

再看 ContentView.swift  页面。

<code>import SwiftUI

struct ContentView: View {
    //这里就没什么讲的了,主要的布局数据
    var body: some View {
        Text("Hello, World!")
            .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

//主要用于预览使用即点击预览区域 resume 的时候显示。
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
</code>

接下来,我们创建一个 LoginView 并且让主窗口先显示我们的Login页面.
选择 项目文件夹 MyApp 右键 新建文件【new File】 在弹出的对话框中 选择 SwiftUI View 格式文档,点击 Next 填写文件名 LoginView
文件创建好以后  我们加入一个按钮,用于后面打开我们的主窗口(至于如何排版,这里就不再细讲),以模拟我们登陆成功以后如何打开主窗口。

<code>import SwiftUI

struct LoginView: View {
    var body: some View {
        //用于读取父类的相关尺寸参数
        GeometryReader { proxy in
          VStack{
            Spacer()
            Text("Hello, Login View!")
            Spacer().frame(width: proxy.size.width, height: 50, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
            Button("打开主窗口"){
                
            }
            Spacer()
          }
        }
    }
}

struct LoginView_Previews: PreviewProvider {
    static var previews: some View {
        LoginView()
    }
}</code>

接下去我们就是要在启动页面修改启动的视图为LoginView 并创建一个 主窗口用于显示主显示内容视图,我们回到 AppDelegate.swift 内容页面,代码修改如下。
 

<code>
import Cocoa
import SwiftUI

@main
class AppDelegate: NSObject, NSApplicationDelegate {

    var mainWin: NSWindow!
    var loginWin: NSWindow!

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        //启动后先显示Login窗口
        let loginView = LoginView()
        loginWin = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 400, height: 300),
            styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
            backing: .buffered, defer: false)
        loginWin.isReleasedWhenClosed = true
        loginWin.center()
        loginWin.setFrameAutosaveName("Login Window")
        loginWin.contentView = NSHostingView(rootView: loginView)
        loginWin.makeKeyAndOrderFront(nil)
    }

    //创建一个方法 用于打开主窗口
    @objc //注册函数可以让obj-c 调用
    func openMainWindow() {
        //如果之前已经创建了,就直接执行后面一句 显示,否则创建
        if nil == mainWin {      // create once !!
            let mainView = ContentView().frame(width: 800, height: 500, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
            mainWin = NSWindow(
                contentRect: NSRect(x: 20, y: 20, width: 800, height: 500),
                styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
                backing: .buffered,
                defer: false)
            mainWin.center()
            mainWin.setFrameAutosaveName("Main Window")
            mainWin.isReleasedWhenClosed = false
            mainWin.contentView = NSHostingView(rootView: mainView)
        }
        mainWin.makeKeyAndOrderFront(nil)
    }
}

</code>

这里我们添加了一个 openMainWindow 的方法 用于打开主窗口界面。
那么 我们需要打开窗口的时候,比如 在LoginView 的页面打开这个窗口 我们可以用 下面的代码打开:

<code>struct LoginView: View {
    var body: some View {
        //用于读取父类的相关尺寸参数
        GeometryReader { proxy in
          VStack{
            Spacer()
            Text("Hello, Login View!")
            Spacer().frame(width: proxy.size.width, height: 50, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
            Button("打开主窗口"){
                NSApp.sendAction(#selector(AppDelegate.openMainWindow), to: nil, from:nil)
            }
            Spacer()
          }
        }
    }
}</code>

我们这里 要调用 AppDelegate.openMainWindow 的方法时,需要用到 NSApp.sendAction 来发送相关的事件,并且 事件为 Selector 的形式,为了检查方便 SwiftUI 给我们提供了 #selector 的标记 用于关联事件。
到这里 我们已经可以从登陆页面中打开我们的主窗口了,但是同时我们发现一个问题,就是两个窗口关闭后 程序依然没有退出,这个时候我们需要在启动页面 AppDelegate.swift 中添加一个方法,

<code>class AppDelegate: NSObject, NSApplicationDelegate {

    var mainWin: NSWindow!
    var loginWin: NSWindow!

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        //启动后先显示Login窗口
        let loginView = LoginView()
        loginWin = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 400, height: 300),
            styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
            backing: .buffered, defer: false)
        loginWin.isReleasedWhenClosed = true
        loginWin.center()
        loginWin.setFrameAutosaveName("Login Window")
        loginWin.contentView = NSHostingView(rootView: loginView)
        loginWin.makeKeyAndOrderFront(nil)
    }

    //创建一个方法 用于打开主窗口
    @objc //注册函数可以让obj-c 调用
    func openMainWindow() {
        //如果之前已经创建了,就直接执行后面一句 显示,否则创建
        if nil == mainWin {      // create once !!
            let mainView = ContentView().frame(width: 800, height: 500, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
            mainWin = NSWindow(
                contentRect: NSRect(x: 20, y: 20, width: 800, height: 500),
                styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
                backing: .buffered,
                defer: false)
            mainWin.center()
            mainWin.setFrameAutosaveName("Main Window")
            mainWin.isReleasedWhenClosed = false
            mainWin.contentView = NSHostingView(rootView: mainView)
        }
        mainWin.makeKeyAndOrderFront(nil)
    }
    //当所有窗口关闭时,退出程序,如果返回false 即不退出程序,如果返回true 即退出。
    func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
        return true
    }
}
</code>

这里以后,我们的窗口如果都关闭后,整个程序就可以退出了。

第三步  关闭登录窗口,并打开主窗口,及设置居中

我们这一步需要在打开主窗口后,如何关闭登录窗口,并设置主窗口在屏幕居中。
在此之前我们需要了解一个全局变量的成员属性 NSApp.keyWindow
这个成员属性 是指 当前应用下 可以接受到 键盘鼠标按键操作的窗口,也就是 我们最后调用了 makeKeyAndOrderFront 后指向的窗口。
所以这里 在我们没有点击打开窗口之前 这个 keyWindow 指向的是我们的登录(loginWin)窗口,打开后指向的是我们的mainWin.
了解了这一步以后我们就知道如何关闭我们的登录窗口 并 设置 主窗口居中了。
修改 LoginView.swift 代码如下。
 

<code>struct LoginView: View {
    var body: some View {
        
        GeometryReader { proxy in
          VStack{
            Spacer()
            Text("Hello, Login View!")
            Spacer().frame(width: proxy.size.width, height: 50, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
            Button("打开主窗口"){
                //这里的 keyWindow 指向的是登录窗口
                NSApp.keyWindow!.close()
                NSApp.sendAction(#selector(AppDelegate.openMainWindow), to: nil, from:nil)
                //这里的keyWindow 指向的是主窗口,因为调用了 AppDelegate.openMainWindow 后 主窗口 makeKeyAndOrderFront 中设置指向了。
                NSApp.keyWindow!.center()
            }
            Spacer()
          }
        }
    }
}
</code>

到这里,我们已经完成了 从登录窗口中打开主窗口,并结束退出登录窗口,我们可以返回去看看 AppDelegate.swift  文件中的代码。
loginWin.isReleasedWhenClosed = true
这一句代码我们可以设置,关闭后销毁对话框,释放loginWin 的内存。

第4步 在窗口之间传值
如果在窗口之间传值,我们需要了解两个东西。
ObservableObject   和  @EnvironmentObject
这里两个玩意一般配套使用,
ObservableObject 是一个观察者对象协议,大概意思是 ObservableObject 下的 @Published 属性包装器包装后的属性 只要修改,就会被swiftUI 观察到,并作出相应的反应。
这里也不细考这个协议,需要了解更多 可以自己网站找补。
而 @EnvironmentObject 是一个依赖注入修饰符,用于注册相应的关联,编译器没有办法根据当前View的具体内容来进行更精确的判断,只要你的View中进行了声明,依赖关系便建立了。

了解这两个玩意后,我们开始编写我们的代码。
我们先创建一个文件 MyFactory.swift 这个文件不要选择SwiftUI 直接选择 swift文件即可。
代码如下:

<code>import SwiftUI

//用于传递值的工厂类
final class MyFactory: ObservableObject {

    @Published var UserName: String = ""

    func setUserName(name: String) {
        UserName = name
    }
}
</code>

然后我们在 启动页面 AppDelegate.swift  中构造实例,并传递给登录和主窗口,这样在登录窗口中修改内容就可以反应到主窗口上面。

<code>class AppDelegate: NSObject, NSApplicationDelegate {

    var mainWin: NSWindow!
    var loginWin: NSWindow!
    var factory = MyFactory() //构造一个用于传值的实例对象

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        //启动后先显示Login窗口
        let loginView = LoginView()
            .environmentObject(factory) //传递factory到登录窗口
        loginWin = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 400, height: 300),
            styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
            backing: .buffered, defer: false)
        loginWin.isReleasedWhenClosed = true
        loginWin.center()
        loginWin.setFrameAutosaveName("Login Window")
        loginWin.contentView = NSHostingView(rootView: loginView)
        loginWin.makeKeyAndOrderFront(nil)
    }

    //创建一个方法 用于打开主窗口
    @objc //注册函数可以让obj-c 调用
    func openMainWindow() {
        //如果之前已经创建了,就直接执行后面一句 显示,否则创建
        if nil == mainWin {      // create once !!
            let mainView = ContentView()
                .frame(width: 800, height: 500, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
                .environmentObject(factory)  //传递factory到主窗口
                
            mainWin = NSWindow(
                contentRect: NSRect(x: 20, y: 20, width: 800, height: 500),
                styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
                backing: .buffered,
                defer: false)
            mainWin.center()
            mainWin.setFrameAutosaveName("Main Window")
            mainWin.isReleasedWhenClosed = false
            mainWin.contentView = NSHostingView(rootView: mainView)
        }
        mainWin.makeKeyAndOrderFront(nil)
    }
    
    func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
        return true
    }
}
</code>

接着 我需要在 LoginView.swift 和 ContentView.swift 页面中接收该参数。
LoginView.swift

<code>struct LoginView: View {
    //注册依赖,以接收传递值
    @EnvironmentObject var factory:MyFactory
    
    var body: some View {
       
        GeometryReader { proxy in
          VStack{
            Spacer()
            Text("Hello, Login View!")
            Spacer().frame(width: proxy.size.width, height: 50, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
            Button("打开主窗口"){
                //设置传值参数
                factory.setUserName(name: "wj008")
                NSApp.keyWindow!.close()
                NSApp.sendAction(#selector(AppDelegate.openMainWindow), to: nil, from:nil)
                NSApp.keyWindow!.center()
            }
            Spacer()
          }
        }
    }
}</code>

ContentView.swift

<code>struct ContentView: View {
    //注册依赖,以接收传递值
    @EnvironmentObject var factory:MyFactory
    //这里就没什么讲的了,主要的布局数据
    var body: some View {
        //读取传值参数
        Text("Hello, My Name:"+factory.UserName)
            .frame(maxWidth: .infinity, maxHeight: .infinity)
        
    }
}
</code>

到这里 我们整个项目的目标就已经基本实现。

另记 SwifUI App 模式

在SwifUI App 项目类型中 ,还有两种打开窗口的方式,
我这里为了区别  项目名为 MyTest
首先 如果我们选择 SwifUI App 创建项目,那么 目录中 就没有 AppDelegate.swift 文件 ,并多了一个 MyTestApp.swift 文件,这个时候我们的程序入口就从该处启动了。
同样 我们需要一个委托类来处理我们推出程序的逻辑。
所以我们修改代码如下。
MyTestApp.swift

<code>import SwiftUI
class AppDelegate: NSObject, NSWindowDelegate, NSApplicationDelegate {
    
    func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
        return true
    }
   
}
@main
struct MyTestApp: App {
    //注入使用委托的类
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    var body: some Scene {
        WindowGroup {
            LoginView().frame(width: 400, height: /*@START_MENU_TOKEN@*/300/*@END_MENU_TOKEN@*/, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
        }
        WindowGroup("MainView"){
            ContentView().frame(width: 800, height: 500, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
                .handlesExternalEvents(preferring: Set(arrayLiteral: "mainview"), allowing: Set(arrayLiteral: "*")) // 这里的用意是防止每次都创建新的窗口,意思是查找窗口存在就直接激活而不重新创建新的窗口
        }
        .handlesExternalEvents(matching: Set(arrayLiteral: "mainview"))
        //这里是添加一个窗口,确保有一个窗口存在就不会退出程序
    }
}
</code>

LoginView.swift

<code>
struct LoginView: View {
    @Environment(.openURL) var openURL
    var body: some View {
       
        GeometryReader { proxy in
          VStack{
            Spacer()
            Text("Hello, Login View!")
            Spacer().frame(width: proxy.size.width, height: 50, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
            Button("打开主窗口"){
                if let url = URL(string: "mytest://mainview") {
                   openURL(url)
                }
            }
            Spacer()
          }
        }
    }
}
</code>

到这里 我们还需要设置一下我们的应用名称,否则我们没有相应的应用路径打开主窗口。
我们先选择我们的项目 
 

学习使用SwiftUI开发MacOS 应用- 第一节 如何创建SwiftUI 应用并实现窗口交互 - 文章图片


设置我们的应用名称,这样我们就可以通过 url mytest://mainview 打开我们的页面。

不过在这里要关闭登录页面 就比较麻烦一些,使用 NSApp.keyWindow 的方式 不能配合 openURL 进行关闭页面,可能期间有一个时间差。
需要先获得 当前视图的窗口 ,修改 LoginView.swift 代码如下.

<code>import SwiftUI

class WindowObserver: ObservableObject {
    weak var window: NSWindow?
}

struct LoginView: View {
    @Environment(.openURL) var openURL
    @StateObject var windowObserver: WindowObserver = WindowObserver()
    var body: some View {
        GeometryReader { proxy in
          VStack{
            Spacer()
            Text("Hello, Login View!")
            Spacer().frame(width: proxy.size.width, height: 50, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
            Button("打开主窗口"){
                if let url = URL(string: "mytest://mainview") {
                    windowObserver.window = NSApp.keyWindow
                    openURL(url)
                    //为啥需要延时关闭,是因为openURL 在打开窗口的时候需要执行一些逻辑,而如果提前把这个窗口关闭 就会执行 applicationShouldTerminateAfterLastWindowClosed 导致程序退出,无法打开主窗口,所以需要等到两个窗口都存在的时候才关闭登录窗口
                    waitToClose()
                }
            }
            Spacer()
          }
        }
    }
    
    func waitToClose(){
        //延时等待窗口打开成功后关闭登录窗口
        DispatchQueue.main.asyncAfter(deadline: .now()+0.01) {
            if(NSApp.windows.count>1){
                windowObserver.window?.close();
                NSApp.keyWindow?.center()
                return
            }else{
                waitToClose()
            }
        }
    }
}

struct LoginView_Previews: PreviewProvider {
    static var previews: some View {
        LoginView()
    }
}
</code>


以上两种模式,我个人建议使用第一种 AppDelegate.swift 模式 可操作性比较大。





 



 


【说明】本文章由站长整理发布,文章内容不代表本站观点,如文中有侵权行为,请与本站客服联系(QQ:254677821)!

相关教程推荐

其他课程推荐