macOS & SwiftUI 2: simplest way to turn off beep on keystroke - macos

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.

Related

How to observe for modifier key pressed (e.g. option, shift) with NSNotification in SwiftUI macOS project?

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

SwiftUI WindowGroup disable window persistence

Apple added new functionality to SwiftUI this year, bringing persistence and multiple windows to our SwiftUI apps. How can we disable window persistence. I'm looking for a windowing system very similar to Xcode, where there's a Welcome window on start, users can open new windows with the content they're looking for, then on the next start of the app only the Welcome window is shown.
The below code achieves all of these goals except the unwanted windows remain
import SwiftUI
#main
struct StackApp: App {
#Environment(\.openWindow) var openWindow
var body: some Scene {
Window("Welcome to App", id: "welcome-to-app") {
VStack {
Text("Welcome")
Button(action: {
openWindow(id: "app-content")
}) {
Text("Open Content")
}
}
}
.defaultSize(CGSize(width: 200, height: 200))
WindowGroup(id: "app-content") {
VStack {
Text("App Content")
}
}
.defaultSize(CGSize(width: 200, height: 200))
}
}
Help is much appreciated
Here's a quick hack proof-of-concept workaround. Can definitely be cleaned up, but it seems to work in macOS 12.6.1.
Not pretty but if you adopt the SwiftUI app lifecycle there just aren't as many ways too hook in and override the system default behavior (can't override the default NSDocumentController etc).
import SwiftUI
#main
struct TestWindowPersistenceApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
DocumentGroup(newDocument: TestWindowPersistenceDocument()) { file in
ContentView(document: file.$document)
}
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationWillFinishLaunching(_ notification: Notification) {
print("did finish launching")
flushSavedWindowState()
// trigger open new file or Welcome flow here
}
func flushSavedWindowState() {
do {
let libURL = try FileManager.default.url(for: .libraryDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
guard let appPersistentStateDirName = Bundle.main.bundleIdentifier?.appending(".savedState") else { print("get bundleID failed"); return }
let windowsPlistFilePath = libURL.appendingPathComponent("Saved Application State", isDirectory: true)
.appendingPathComponent(appPersistentStateDirName, isDirectory: true)
.appendingPathComponent("windows.plist", isDirectory: false)
.path
print("path to remove: ", windowsPlistFilePath)
try FileManager.default.removeItem(atPath: windowsPlistFilePath)
} catch {
print("exception: \(error)")
}
}
}
Check out my Swift package which should solve this problem.
You could use it like this:
#main
struct MyApp: App {
var body: some Scene {
WindowGroup(id: "MyWindow") {
ContentView()
}
.register("MyWindow")
.disableRestoreOnLaunch()
}
}
It seems to work fine and achieved this by setting the isRestorable property of NSWindow to false. This should disable the default behavior.

Xcode 12 SwiftUI Preview not highlighting Views embedded in NavigationView

Using the SwiftUI Xcode preview pane, I can click on the rendered views and see the code responsible for that view highlighted in the code pane (and vice versa). However, when I embed the view in a NavigationView or sometimes even addding a .navigationBarTitle() to the root view, I can't do that anymore. All views in the Navigation group just becomes one big view and the previewer can't identify the separate underlying pieces anymore.
Is this a bug? Is there a way to get around it? It's a really useful feature and most of my views sit in some kind of Navigation object.
Working
import SwiftUI
struct NavSample: View {
var body: some View {
Text("Hello, World!")
}
}
struct NavSample_Previews: PreviewProvider {
static var previews: some View {
NavSample()
}
}
Not Working
import SwiftUI
struct NavSample: View {
var body: some View {
NavigationView { // Adding this
Text("Hello, World!")
}
}
}
struct NavSample_Previews: PreviewProvider {
static var previews: some View {
NavSample()
}
}

Streamlining SwiftUI Previews in Xcode

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

Is there a reliable workaround for onDisappear() not working within .sheet() or .popover() in SwiftUI on macOS?

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?

Resources