5

There's a lot of code my app normally runs that I would like to skip in Previews. Code that is time-consuming and has no visible effects (such as initializing audio devices). I'm trying to figure out how skip it for previews.

There is an easy way to run code only in the production build of an app using the DEBUG macro. But I don't know of anything similar for non-Preview builds (because Previews presumably build the same code as non-Previews).

I thought that setting a variable, previewMode, within my ViewModel, would work. That way I could set it to true only within the PreviewProvider:

struct MainView_Previews: PreviewProvider {

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

and when I created the ViewModel within the SceneDelegate, I could set previewMode to 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()
    }
}

so that I can enclose any code I don't want to run for previews in if !previewMode { ••• }

Unfortunately the code is still running. Evidently the scene() function is getting called whenever my preview updates. :(

How can I specify code to not run for previews?

thanks!

Anton
  • 2,512
  • 2
  • 20
  • 36
  • The answer is available [here](https://stackoverflow.com/questions/58759987/how-do-you-check-if-swiftui-is-in-preview-mode). – Anton Oct 13 '20 at 17:35

2 Answers2

3

Practically Live-Preview mode run-time does not differ much from Simulator Debug mode run-time. And this, of course, as intended to give us quick (as possible) feedback of our code execution.

Anyway here are some findings... that might be used as solution/workaround for some cases that detection of Preview is highly desirable.

So created from scratch SwiftUI Xcode template project and in all functions of generated entities add print(#function) instruction.

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

Perform Debug Preview and see output:

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

As it is clear complete workflow of app launching has been performed at start AppDelegate > SceneDelegate > ContentView > Window and only after this the PreviewProvider part.

And in this latter part we see something interesting - all functions of ContentView in Preview mode have __preview prefix (except init)!!

So, finally, here is possible workaround (DISCLAIMER!!! - on your own risk - only demo)

The following variant

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!")
        }
    }
}

Gives this

demo

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • Thank you, @Asperi! I'll give it a go. I'm not worried about dangers for what you propose because your method can only err in the direction of allowing non-preview behaviors in preview mode (which is what I've got already) rather than preview behaviors in non-preview mode (which would be a big problem). But in practice the functions that I want to simplify in preview mode are actually the init functions called in scene(_:willConnectTo:options:)… so I'll see whether this will work for that. And, in any case, I didn't know about #function so thank you already for teaching me that! – Anton Jun 12 '20 at 17:57
  • I was able to test this. Yes, it seems to work in all the code for Views. Unfortunately the code I write always triggers initializations—the actions I want to avoid running—within scene(_:willConnectTo:options:). That way the results of the initializations are attached as environmentObjects or initializer arguments to the root view when it is created & attached to the window. I could change the code to trigger initializations in the root View's onAppear()… though I'd have to reorganize some things as a result, and it wouldn't be ideal. So, other ideas are still welcome—but thanks for this! – Anton Jun 14 '20 at 22:33
2

The only working solution I've found is to use the ProcessInfo.processInfo.environment value for key XCODE_RUNNING_FOR_PREVIEWS. It's set to "1" only when running in preview mode:

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

See this post.

Anton
  • 2,512
  • 2
  • 20
  • 36