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!
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.
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
Related
I want to have a Bool property, that represents that option key is pressed #Publised var isOptionPressed = false. I would use it for changing SwiftUI View.
For that, I think, that I should use Combine to observe for key pressure.
I tried to find an NSNotification for that event, but it seems to me that there are no any NSNotification, that could be useful to me.
Since you are working through SwiftUI, I would recommend taking things just a step beyond watching a Publisher and put the state of the modifier flags in the SwiftUI Environment. It is my opinion that it will fit in nicely with SwiftUI's declarative syntax.
I had another implementation of this, but took the solution you found and adapted it.
import Cocoa
import SwiftUI
import Combine
struct KeyModifierFlags: EnvironmentKey {
static let defaultValue = NSEvent.ModifierFlags([])
}
extension EnvironmentValues {
var keyModifierFlags: NSEvent.ModifierFlags {
get { self[KeyModifierFlags.self] }
set { self[KeyModifierFlags.self] = newValue }
}
}
struct ModifierFlagEnvironment<Content>: View where Content:View {
#StateObject var flagState = ModifierFlags()
let content: Content;
init(#ViewBuilder content: () -> Content) {
self.content = content();
}
var body: some View {
content
.environment(\.keyModifierFlags, flagState.modifierFlags)
}
}
final class ModifierFlags: ObservableObject {
#Published var modifierFlags = NSEvent.ModifierFlags([])
init() {
NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { [weak self] event in
self?.modifierFlags = event.modifierFlags
return event;
}
}
}
Note that my event closure is returning the event passed in. If you return nil you will prevent the event from going farther and someone else in the system may want to see it.
The struct KeyModifierFlags sets up a new item to be added to the view Environment. The extension to EnvironmentValues lets us store and
retrieve the current flags from the environment.
Finally there is the ModifierFlagEnvironment view. It has no content of its own - that is passed to the initializer in an #ViewBuilder function. What it does do is provide the StateObject that contains the state monitor, and it passes it's current value for the modifier flags into the Environment of the content.
To use the ModifierFlagEnvironment you wrap a top-level view in your hierarchy with it. In a simple Cocoa app built from the default Xcode template, I changed the application SwiftUI content to be:
struct KeyWatcherApp: App {
var body: some Scene {
WindowGroup {
ModifierFlagEnvironment {
ContentView()
}
}
}
}
So all of the views in the application could watch the flags.
Then to make use of it you could do:
struct ContentView: View {
#Environment(\.keyModifierFlags) var modifierFlags: NSEvent.ModifierFlags
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
if(modifierFlags.contains(.option)) {
Text("Option is pressed")
} else {
Text("Option is up")
}
}
.padding()
}
}
Here the content view watches the environment for the flags and the view makes decisions on what to show using the current modifiers.
Ok, I found easy solution for my problem:
class KeyPressedController: ObservableObject {
#Published var isOptionPressed = false
init() {
NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { [weak self] event -> NSEvent? in
if event.modifierFlags.contains(.option) {
self?.isOptionPressed = true
} else {
self?.isOptionPressed = false
}
return nil
}
}
}
I have a SwiftUI app that I am creating. Upon the user closing the last window, I would like to prompt the user and inform them that the app will also quit.
I have taken a look at both the solutions for creating an alert upon app quiting here and have also looked at the solution for closing the application when the last window closes here.
Both of which I have gotten to work however, not together. What I am looking for is a way to detect when a user closes the last window in the application, then prompt the user with an alert letting them know it will quit the application and asking if they would like to continue or cancel.
Using .onDisappear does not seem to work. I have implemented a appDelegate and it's applicationShouldTerminateAfterLastWindowClosed method, but when the last window closes, it does not seem to prompt the .alert behavior in my application.
Application class
class Application: NSObject, NSApplicationDelegate, ObservableObject {
#Published var willTerminate = false
override init() {
super.init()
}
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
if NSApplication.shared.windows.count == 0 {
return .terminateNow
}
self.willTerminate = true
return .terminateLater
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
func resume() {
NSApplication.shared.reply(toApplicationShouldTerminate: false)
}
func close() {
NSApplication.shared.reply(toApplicationShouldTerminate: true)
}
}
struct WindowAccessor: NSViewRepresentable {
#Binding var window: NSWindow?
func makeNSView(context: Context) -> NSView {
let view = NSView()
DispatchQueue.main.async {
self.window = view.window
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {}
}
ContentView
struct ContentView: View {
#State private var window: NSWindow?
#EnvironmentObject private var appDelegate: Application
var body: some View {
ZStack {
MyView()
// ...
.onDisappear(
// Code in here does not run when WindowAccessor is set to background
})
.background(WindowAccessor(window: self.$window))
.alert(isPresented: Binding<Bool>(get: { self.appDelegate.willTerminate && self.window?.isKeyWindow ?? false }, set: { self.appDelegate.willTerminate = $0 }), content: {
SoloLogger(for: .window).coreLog(message: "ApplicationClosedEvent", level: .info)
return Alert(title: Text("Quit Application?"),
message: Text("Do you really want to quit the application?"),
primaryButton: .default(Text("Cancel"), action: {self.appDelegate.resume() }),
secondaryButton: .destructive(Text("Quit"), action: {self.appDelegate.close()}))
})
}
}
}
I've been working on something similar.
You can pick up the #AppDelegate from the environment and don't need to create a WindowAccessor.
I created a view which can be added into your content view's ZStack:
struct MacOSQuitCheckView: View {
// MARK: - PROPERTIES
#EnvironmentObject private var appDelegate: AppDelegate
// MARK: - VIEW BODY
var body: some View {
EmptyView()
.alert("App wants to quit?"), isPresented: isPresented) {
Button("Do not quit", role: .cancel, action: appDelegate.resume)
Button("Quit", action: appDelegate.close)
}
}
// MARK: - PRIVATE COMPUTED PROPERTIES
private var isPresented: Binding<Bool> {
Binding<Bool>(get: { self.appDelegate.willTerminate }, set: { self.appDelegate.willTerminate = $0 })
}
}
The following trivial macOS app is written in SwiftUI 2.0.
import SwiftUI
#main
struct TempApp: App {
var body: some Scene {
WindowGroup { ContentView() }
}
}
struct ContentView: View {
var body: some View {
Text("Hello, beep!").padding()
}
}
When in the foreground, this app will emit an error beep on certain keystrokes (like "a"). What's the simplest way to suppress this beep?
An Xcode project illustrating this (and the answer) can be found here.
There are many older related questions on SO, but none of these are specifically about doing this in SwiftUI 2.0.
You can suppress the beep by adding a local monitor for the .keyDown event at the top level. This can be done simply in ContentView.init(), like so:
struct ContentView: View {
var body: some View {
Text("Hello, silence!").padding()
}
init() {
NSEvent.addLocalMonitorForEvents(matching: .keyDown) { _ in return nil }
}
}
This technique was inspired by this answer.
I am saving an Int in UserDefaults and this is reduced by one by clicking a button. I don't know if that is important but I have added an extension to UserDefaults to load an initial value if the app starts for the first time:
extension UserDefaults {
public func optionalInt(forKey defaultName: String) -> Int? {
let defaults = self
if let value = defaults.value(forKey: defaultName) {
return value as? Int
}
return nil
}
}
The UserDefaults are used as ObservableObject and accessed as EnvironmentObject within the app like this:
class Preferences: ObservableObject {
#Published var counter: Int = UserDefaults.standard.optionalInt(forKey: COUNTER_KEY) ?? COUNTER_DEFAULT_VALUE {
didSet {
UserDefaults.standard.set(counter, forKey: COUNTER_KEY)
}
}
}
I am now trying to test that the value in the UserDefaults decreases when the button is clicked.
I am trying to read the UserDefaults in the test with:
XCTAssertEqual(UserDefaults.standard.integer(forKey: "COUNTER_KEY"), 9)// default is 10
I have tried it with normal UnitTests where the methods behind the Button are called and with UITests but both do not work. In the UnitTests I get back the COUNTER_DEFAULT_VALUE and in the UiTests I get back 0.
I am trying to access the UserDefaults directly in the test, instead of using the Preferences object, because I have not found a way to access that as it is an ObservableObject.
I have checked in the Emulator that the UserDefaults are saved/loaded correctly when using the app. Is it not possible to access the UserDefaults in the tests or am I doing it wrong?
The key to success is Dependency injection. Instead of directly accessing the shared user defaults object (UserDefaults.standard), declare an object of type UserDefaults within the class:
let userDefaults: UserDefaults
In the view where you declare the model, you are free to use the shared object:
#StateObject var model = Preferences(userDefaults: UserDefaults.standard)
But inside your test, create an dedicated UserDefaults object and pass it to the initializer like so:
let userDefaults = UserDefaults(suiteName: #file)!
userDefaults.removePersistentDomain(forName: #file)
let model = Preferences(userDefaults: userDefaults)
The benefit is clear: You control the state of UserDefaults. And that means the code works in every environment. To keep it simple, I haven't incorporated your extension, yet. But I'm sure you will manage to get it working.
TL;DR
Please see my working example below:
ContentView.swift
import SwiftUI
import Foundation
class Preferences: ObservableObject {
let userDefaults: UserDefaults
#Published var counter: Int {
didSet {
self.userDefaults.set(counter, forKey: "myKey")
}
}
init(userDefaults: UserDefaults) {
self.userDefaults = userDefaults
self.counter = userDefaults.integer(forKey: "myKey")
}
func decreaseCounter() {
self.counter -= 1
}
}
struct ContentView: View {
#StateObject var model = Preferences(userDefaults: UserDefaults.standard)
var body: some View {
HStack {
Text("Value: \(self.model.counter)")
Button("Decrease") {
self.model.decreaseCounter()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
InjectionTests.swift
import XCTest
#testable import Injection
class InjectionTests: XCTestCase {
func testPreferences() throws {
// arrange
let userDefaults = UserDefaults(suiteName: #file)!
userDefaults.removePersistentDomain(forName: #file)
let model = Preferences(userDefaults: userDefaults)
// act
let valueBefore = userDefaults.integer(forKey: "myKey")
model.decreaseCounter()
let valueAfter = userDefaults.integer(forKey: "myKey")
// assert
XCTAssertEqual(valueBefore - 1, valueAfter)
}
}
I'm building an app that shares quite a bit of SwiftUI code between its iOS and macOS targets. On iOS, onDisappear seems to work reliably on Views. However, on macOS, onDisappear doesn't get called if the View is inside a sheet or popover.
The following code illustrates the concept:
import SwiftUI
struct ContentView: View {
#State private var textShown = true
#State private var showSheet = false
#State private var showPopover = false
var body: some View {
VStack {
Button("Toggle text") {
self.textShown.toggle()
}
if textShown {
Text("Text").onDisappear {
print("Text disappearing")
}
}
Button("Toggle sheet") {
self.showSheet.toggle()
}.sheet(isPresented: $showSheet, onDismiss: {
print("On dismiss")
}) {
VStack {
Button("Close sheet") {
self.showSheet = false
}
}.onDisappear {
print("Sheet disappearing")
}
}
Button("Toggle popover") {
self.showPopover.toggle()
}.popover(isPresented: $showPopover) {
VStack {
Text("popover")
}.onDisappear {
print("Popover disappearing")
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
Note that onDisappear works fine on the Text component at the beginning of the VStack but the other two onDisappear calls don't get executed on macOS.
One workaround I've found is to attach an ObservableObject to the View and use deinit to call cleanup code. However, this isn't a great solution for two reasons:
1) With the popover example, there's a significant delay between the dismissal of the popover and the deist call (although it works quickly on sheets)
2) I haven't had any crashes on macOS with this approach, but on iOS, deinit have been unreliable in SwiftUI doing anything but trivial code -- holding references to my data store, app state, etc. have had crashes.
Here's the basic approach I used for the deinit strategy:
class DeinitObject : ObservableObject {
deinit {
print("Deinit obj")
}
}
struct ViewWithObservableObject : View {
#ObservedObject private var deinitObj = DeinitObject()
var body: some View {
Text("Deinit view")
}
}
Also, I would have thought I could use the onDismiss parameter of the sheet call, but that doesn't get called either on macOS. And, it's not an available parameter of popover.
All of this is using Xcode 11.4.1 and macOS 10.15.3.
Any solutions for good workarounds?