第一句子网 - 唯美句子、句子迷、好句子大全
第一句子网 > 学习使用SwiftUI开发MacOS应用 - 第1节 如何创建SwiftUI 应用并实现窗口交互

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

时间:2020-11-12 18:19:50

相关推荐

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

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

第一步 创建项目

我们先学习如何创建SwiftUI项目,和在项目中选择何种方式去创建SwiftUI 项目,即两种方式创建SwiftUI项目的区别和在实际项目中如何使用他们。

首先我们打开 Xcode 创建项目,在项目中我们选择 macOS 类型的项目,并选择 App 点击 next

进入到项目参数选择界面

先填写项目名称 我这里填写 MyApp 在选择 Life Cycle 为 AppKit App Delegate 项目类型,这里还有一个 SwiftUI App

我们先看看AppKit App Delegate 的代码目录结构 ,这里是我们比较关心的文件

AppDelegate.swift 文件中 主要有启动项目的相关设置代码。项目的启动是从这里开始的。

ContentView.swift 这个是我们窗口的内容视图页面,也是我们主要的开发编辑视图的文件。

Assets.xcassets 这个是我们项目用到的资源文件目录,以及相应的资源设置目录

Main.storyboard 这个是我们顶部菜单,及其他菜单,等相关设置,在 info.plist 指定了这个文件,所以不要删除。

info.plist 是苹果的一些配置信息文件。

MyApp.entitlements 文件是一些权限配置,服务调用配置,证书配置等相关的配置文件。

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

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

我们先打开AppDelegate.swift 文件。

import Cocoaimport SwiftUI//这里注册为主执行函数类@mainclass 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}}

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

再看 ContentView.swift 页面。

import SwiftUIstruct 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()}}

接下来,我们创建一个 LoginView 并且让主窗口先显示我们的Login页面.

选择 项目文件夹 MyApp 右键 新建文件【new File】 在弹出的对话框中 选择 SwiftUI View 格式文档,点击 Next 填写文件名 LoginView

文件创建好以后 我们加入一个按钮,用于后面打开我们的主窗口(至于如何排版,这里就不再细讲),以模拟我们登陆成功以后如何打开主窗口。

import SwiftUIstruct LoginView: View {var body: some View {//用于读取父类的相关尺寸参数GeometryReader { proxy inVStack{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()}}

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

import Cocoaimport SwiftUI@mainclass 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 = trueloginWin.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 = falsemainWin.contentView = NSHostingView(rootView: mainView)}mainWin.makeKeyAndOrderFront(nil)}}

这里我们添加了一个 openMainWindow 的方法 用于打开主窗口界面。

那么 我们需要打开窗口的时候,比如 在LoginView 的页面打开这个窗口 我们可以用 下面的代码打开:

struct LoginView: View {var body: some View {//用于读取父类的相关尺寸参数GeometryReader { proxy inVStack{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()}}}}

我们这里 要调用AppDelegate.openMainWindow 的方法时,需要用到NSApp.sendAction 来发送相关的事件,并且 事件为 Selector 的形式,为了检查方便 SwiftUI 给我们提供了 #selector 的标记 用于关联事件。

到这里 我们已经可以从登陆页面中打开我们的主窗口了,但是同时我们发现一个问题,就是两个窗口关闭后 程序依然没有退出,这个时候我们需要在启动页面AppDelegate.swift中添加一个方法,

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 = trueloginWin.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 = falsemainWin.contentView = NSHostingView(rootView: mainView)}mainWin.makeKeyAndOrderFront(nil)}//当所有窗口关闭时,退出程序,如果返回false 即不退出程序,如果返回true 即退出。func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {return true}}

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

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

我们这一步需要在打开主窗口后,如何关闭登录窗口,并设置主窗口在屏幕居中。

在此之前我们需要了解一个全局变量的成员属性NSApp.keyWindow

这个成员属性 是指 当前应用下 可以接受到 键盘鼠标按键操作的窗口,也就是 我们最后调用了makeKeyAndOrderFront 后指向的窗口。

所以这里 在我们没有点击打开窗口之前 这个 keyWindow 指向的是我们的登录(loginWin)窗口,打开后指向的是我们的mainWin.

了解了这一步以后我们就知道如何关闭我们的登录窗口 并 设置 主窗口居中了。

修改 LoginView.swift 代码如下。

struct LoginView: View {var body: some View {GeometryReader { proxy inVStack{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()}}}}

到这里,我们已经完成了 从登录窗口中打开主窗口,并结束退出登录窗口,我们可以返回去看看AppDelegate.swift 文件中的代码。

loginWin.isReleasedWhenClosed = true

这一句代码我们可以设置,关闭后销毁对话框,释放loginWin 的内存。

第4步 在窗口之间传值

如果在窗口之间传值,我们需要了解两个东西。

ObservableObject 和@EnvironmentObject

这里两个玩意一般配套使用,

ObservableObject 是一个观察者对象协议,大概意思是 ObservableObject 下的 @Published 属性包装器包装后的属性 只要修改,就会被swiftUI 观察到,并作出相应的反应。

这里也不细考这个协议,需要了解更多 可以自己网站找补。

而@EnvironmentObject 是一个依赖注入修饰符,用于注册相应的关联,编译器没有办法根据当前View的具体内容来进行更精确的判断,只要你的View中进行了声明,依赖关系便建立了。

了解这两个玩意后,我们开始编写我们的代码。

我们先创建一个文件MyFactory.swift 这个文件不要选择SwiftUI 直接选择 swift文件即可。

代码如下:

import SwiftUI//用于传递值的工厂类final class MyFactory: ObservableObject {@Published var UserName: String = ""func setUserName(name: String) {UserName = name}}

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

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 = trueloginWin.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 = falsemainWin.contentView = NSHostingView(rootView: mainView)}mainWin.makeKeyAndOrderFront(nil)}func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {return true}}

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

LoginView.swift

struct LoginView: View {//注册依赖,以接收传递值@EnvironmentObject var factory:MyFactoryvar body: some View {GeometryReader { proxy inVStack{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()}}}}

ContentView.swift

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

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

另记SwifUI App 模式

在SwifUI App 项目类型中 ,还有两种打开窗口的方式,

我这里为了区别 项目名为 MyTest

首先 如果我们选择SwifUI App 创建项目,那么 目录中 就没有AppDelegate.swift 文件 ,并多了一个 MyTestApp.swift 文件,这个时候我们的程序入口就从该处启动了。

同样 我们需要一个委托类来处理我们推出程序的逻辑。

所以我们修改代码如下。

MyTestApp.swift

import SwiftUIclass AppDelegate: NSObject, NSWindowDelegate, NSApplicationDelegate {func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {return true}}@mainstruct MyTestApp: App {//注入使用委托的类@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegatevar 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"))//这里是添加一个窗口,确保有一个窗口存在就不会退出程序}}

LoginView.swift

struct LoginView: View {@Environment(\.openURL) var openURLvar body: some View {GeometryReader { proxy inVStack{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()}}}}

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

我们先选择我们的项目

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

不过在这里要关闭登录页面 就比较麻烦一些,使用 NSApp.keyWindow 的方式 不能配合 openURL 进行关闭页面,可能期间有一个时间差。

需要先获得 当前视图的窗口 ,修改 LoginView.swift 代码如下.

import SwiftUIclass WindowObserver: ObservableObject {weak var window: NSWindow?}struct LoginView: View {@Environment(\.openURL) var openURL@StateObject var windowObserver: WindowObserver = WindowObserver()var body: some View {GeometryReader { proxy inVStack{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.keyWindowopenURL(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()}}

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

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。