在 Xcode 中简化 SwiftUI 预览

Streamlining SwiftUI Previews in Xcode

我的应用程序通常 运行 有很多代码我想在预览中跳过。耗时且没有可见效果的代码(例如初始化音频设备)。我想弄清楚如何跳过它进行预览。

有一种简单的方法可以 运行 使用 DEBUG 宏仅在应用程序的生产构建中编码。但是我不知道非预览版本有什么类似的东西(因为预览可能会构建与非预览相同的代码)。

我认为在我的 ViewModel 中设置一个变量 previewMode 是可行的。这样我就可以仅在 PreviewProvider:

中将其设置为 true
struct MainView_Previews: PreviewProvider {

    static var previews: some View {
        let vm = ViewModel(previewMode: true)
        return MainView(viewModel: vm)
    }
}

当我在 SceneDelegate 中创建 ViewModel 时,我可以将 previewMode 设置为 false:

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

    let vm = ViewModel(previewMode: false)
    let mainView = MainView(viewModel: vm)

    // Use a UIHostingController as window root view controller.
    if let windowScene = scene as? UIWindowScene {
        let window = UIWindow(windowScene: windowScene)
        window.rootViewController = UIHostingController(rootView: mainView)
        self.window = window
        window.makeKeyAndVisible()
    }
}

这样我就可以在 if !previewMode { ••• }

中附上任何我不想 运行 预览的代码

不幸的是代码仍然是 运行ning。显然,每当我的预览更新时,都会调用 scene() 函数。 :(

如何指定代码不 运行 进行预览?

谢谢!

实际上实时预览模式 运行-time 与模拟器调试模式 运行-time 没有太大区别。当然,这旨在为我们提供代码执行的快速(尽可能)反馈。

无论如何,这里有一些发现...在某些情况下 solution/workaround 非常需要检测预览。

因此从头开始创建 SwiftUI Xcode 模板项目,并在生成的实体的所有功能中添加 print(#function) 指令。

ContentView.swift

import SwiftUI

struct ContentView: View {
    init() {
        print(#function)
    }

    var body: some View {
        print(#function)
        return someView()
            .onAppear {
                print(#function)
            }
    }

    private func someView() -> some View {
        print(#function)
        return Text("Hello, World!")
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        print(#function)
        return ContentView()
    }
}

执行调试预览并查看输出:

application(_:didFinishLaunchingWithOptions:)
application(_:configurationForConnecting:options:)
scene(_:willConnectTo:options:)
init()
sceneWillEnterForeground(_:)
sceneDidBecomeActive(_:)
2020-06-12 16:08:14.460096+0300 TestPreview[70945:1547508] [Agent] Received remote injection
2020-06-12 16:08:14.460513+0300 TestPreview[70945:1547508] [Agent] Create remote injection Mach transport: 6000026c1500
2020-06-12 16:08:14.460945+0300 TestPreview[70945:1547482] [Agent] No global connection handler, using shared user agent
2020-06-12 16:08:14.461216+0300 TestPreview[70945:1547482] [Agent] Received connection, creating agent
2020-06-12 16:08:15.355019+0300 TestPreview[70945:1547482] [Agent] Received message: < DTXMessage 0x6000029c94a0 : i2.0e c0 object:(__NSDictionaryI*) {
    "updates" : <NSArray 0x7fff8062cc40 | 0 objects>
    "id" : [0]
    "scaleFactorHint" : [3]
    "providerName" : "11TestPreview20ContentView_PreviewsV"
    "products" : <NSArray 0x600000fcc650 | 1 objects>
} > {
    "serviceCommand" : "forwardMessage"
    "type" : "display"
}
__preview__previews
init()
__preview__body
__preview__someView()
__preview__body
__preview__body
__preview__someView()
__preview__body

很明显,应用程序启动的完整工作流程已在 AppDelegate > SceneDelegate > ContentView > Window 开始执行,仅在 PreviewProvider 之后执行部分。

在后面的部分我们看到了一些有趣的东西——预览模式下 ContentView 的所有函数都有 __preview 前缀(init 除外)!!

所以,最后,这里有一个可能的解决方法 (免责声明!!! - 风险自负 - 仅演示)

以下变体

struct ContentView: View {

    var body: some View {
        return someView()
            .onAppear {
                if #function.hasPrefix("__preview") {
                    print("Hello Preview!")
                } else {
                    print("Hello World!")
                }
            }
    }

    private func someView() -> some View {
        if #function.hasPrefix("__preview") {
            return Text("Hello Preview!")
        } else {
            return Text("Hello World!")
        }
    }
}

给这个

我找到的唯一可行的解​​决方案是使用键 XCODE_RUNNING_FOR_PREVIEWSProcessInfo.processInfo.environment 值。仅当 运行 处于预览模式时才设置为“1”:

let previewMode: Bool = ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"

参见