肇鑫的技术博客

业精于勤,荒于嬉

为什么使用SwiftUI常常会遇到莫名其妙的问题

在谈这个问题之前,我想先谈谈我所理解的苹果开发框架。会看这篇文章的人,应该绝大多书都是搞iOS开发的,那么我们就从iOS开发的角度来谈,捎带macOS开发的角度。

在没有SwiftUI之前,iOS开发都是使用UIKit,相应的macOS的开发使用AppKit。它们也被称为Cocoa Touch和Cocoa。而实际上,虽然UIKit能够实现开发者想要实现的大部分功能。但是某些涉及到硬件或者网络底层功能的时候,开发者还是需要使用另外的框架。

如果从开发语言的角度看,iOS/macOS的最底层的系统,是使用C语言和C++来开发的。被称作Core Foundation。而上一层,则是使用Objective-C语言开发的Foundation。和Foundation同层次的,是同样用Objective-C开发的UIKit和AppKit。

有了Swift之后,苹果在用Swift重写Foundation这一层,最近这几年的WWDC,几乎年年都有Foundation用Swift重写部分功能之后,性能提升的消息。但其实,这所谓的性能提升,本质上讲就是用静态类型的语言,替换动态类型语言换来的。也许有人会问,为啥苹果不用Swift重写更底层的Core Foundation呢?答案很简单,因为C和C++也是静态类型的语言,用Swift重写,也不会有额外的性能提升。

说了半天,下面言归正传,回归到SwiftUI的问题。SwiftUI是基于UIKit/AppKit的。所以如果说后者能做的事是100,那么SwiftUI只能做到其中的7到8成。这与之前讨论的情形类似。

结论:越是高级层次的框架,使用起来越简单。但是同时,支持的功能也相对越少。我们需要的功能越多,越细,就越需要调用更深层次的框架。

双击打开文件SwiftUI版本的实现(进阶篇)

阅读此篇之前,建议先阅读基础篇。双击打开文件SwiftUI版本的实现(基础篇)

本篇,我们要实现类似“Welcome to Xcode”这样的欢迎界面。当用户直接打开应用时,显示欢迎界面,而如果用户双击指定类型的文件,则不显示这个欢迎界面,而直接显示应用的主界面。

welcome

问题分析

首先想到的就是,开机直接显示欢迎界面,然后在其中处理文件的打开。测试显示,此路不通。

规则1: openURL的调用会自动选择接受该参数的WindowGroup

Window("WelcomeViewWindow", id: "WelcomeViewWindow") {
    WelcomeView(showFileImporter: $showFileImporter)
}
.windowStyle(.hiddenTitleBar)
.windowResizability(.contentSize)

WindowGroup(for: URL.self) { $fileURL in
    MainView(fileURL: $fileURL)
        .environment(\.managedObjectContext, persistenceController.container.viewContext)
}

虽然两个View都支持openURL,但是之后第二组的openURL会被调用,因为传递来的URL参数,只有第二组支持。

同时,也出现了一个问题。就是虽然只有第二组的openURL会被调用,但是欢迎窗口总是打开。

规则2: 位于首位的WindowGroup总是被打开

这是因为规则2的存在。解决办法也很简单,将两个WindowsGroup掉个个即可。同时,欢迎界面的openURL也可以删掉了。因为根本不会调用到。

遇到苹果API的bug

这样,打开文件的问题就解决了。不过,我们最初希望的是用户直接打开应用的时候,显示欢迎界面,而现在显示的是空白的主界面。这不是我们想要的。

解决办法也很简单,我们在主界面的onAppear阶段,检测fileURL,如果它是nil,就证明是直接打开的应用,我们就调用欢迎界面,然后通过dismiss关闭主界面。

.onAppear {
    if fileURL == nil {
        openWindow(id: "WelcomeViewWindow")
        dismiss()
    } 
}

可问题是,实际测试时,主界面每次都不能成功关闭。通过阅读dismiss的文档,我发现,dismiss能关闭窗口的前提是,ScenePhase必须是.active的状态。经过检测发现,主界面当前的ScenePhase却是.background的状态。

DismissAction

这明显是苹果API出了bug,于是我和苹果提交了反馈。有bug,生活还得继续。既然dismiss关闭不了,我们可以直接关闭窗口。我的解决方案是绑定Window,然后直接关闭这个Window。

private func quit() {
    if let window {
        window.close()
    } else {
        DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100), execute: DispatchWorkItem(block: quit))
    }
}

SwiftUIWindowBinder

解决openURL和onAppear调用顺序的问题

需要注意的事,SwiftUI中,二者是先onAppear,然后才调用openURL。所以,如果我们需要改变这个顺序,就需要将onAppear运行的代码,加入到DispatchQueue.main.async中,然后推后一个周期。

双击打开文件SwiftUI版本的实现(基础篇)

我们知道,在使用AppKit的时候,我们通过NSApplicationDelegate的几个open方法来实现双击打开文件。

@MainActor optional func application(_ application: NSApplication, open urls: [URL])
@MainActor optional func application(_ sender: NSApplication, openFile filename: String) -> Bool
@MainActor optional func application(_ sender: NSApplication, openFiles filenames: [String])

但是在SwiftUI下,情况就有所不同了。我们这里所说的SwiftUI,指的是应用以SwiftUI的框架为主,而不是以AppKit为主,混合使用SwiftUI的情况。后者,和上面的情况没区别。

SwiftUI应用带来的变化

首先是,NSApplicationDelegate的这三个方法都发生了改变,后两个方法直接不会被调用了。而第一个方法经常会返回一个空的数组。这让人非常疑惑。

Handle multiple URLs from Open With in SwiftUI on Mac

经过查询,我知道,原来在SwiftUI下,SwiftUI的View本身可以处理一个打开的URL,而多余的,则需要用

@MainActor optional func application(_ application: NSApplication, open urls: [URL])

来进行处理。

吐个槽:这么做实在有些弱智。

所以我们所需要的正确做法就是:

  1. 在SwiftUI的应用中,使用onOpenURL(perform:)处理单个文件的双击打开。
  2. 如果是多个文件被双击打开,则通过NSApplicationDelegateapplication(_:open:)方法处理剩下的文件。

进阶篇

双击打开文件SwiftUI版本的实现(进阶篇)