Swift Command Line Tool Not Receiving DistributedNotificationCenter Notifications - swift

I am trying to create a very basic Swift command-line application that signals to another application using a WebSocket when the macOS UI changes to/from light/dark mode.
For some reason, the command-line tool is not receiving any notifications from DistributedNotificationCenter, in particular, AppleInterfaceThemeChangedNotification. However, running the exact same code in a Cocoa UI app on applicationDidFinishLaunching works perfectly fine.
I found an old Obj-C CLI project on Github that is meant to print out every notification, but that doesn't do anything either. It makes me suspect Apple perhaps changed something, but I cannot seem to find anything online about it. Are there certain Xcode project settings I need to set?
// main.swift
import Foundation
class DarkModeObserver {
func observe() {
print("Observing")
DistributedNotificationCenter.default.addObserver(
forName: Notification.Name("AppleInterfaceThemeChangedNotification"),
object: nil,
queue: nil,
using: self.interfaceModeChanged(notification:)
)
}
func interfaceModeChanged(notification: Notification) {
print("Notification", notification)
}
}
let observer = DarkModeObserver.init()
observer.observe()
RunLoop.main.run()

I managed to get iTunes notifications working, so it was just the theme change notifications that weren't working. Given this, I suspect Apple only sends the notifications to UI/NSApplication applications. As such, replacing the last 3 lines from above with the following works:
let app = NSApplication.shared
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
let observer = DarkModeObserver.init()
observer.observe()
}
}
let delegate = AppDelegate()
app.delegate = delegate
app.run()

Related

Firebase exception error for macOS app Xcode

When I try to run the simulator for my macOS app, which is using Firebase, it gives this error: "Thread 1: "The default FirebaseApp instance must be configured before the default Authinstance can be initialized. One way to ensure this is to call FirebaseApp.configure() in the App Delegate's application(_:didFinishLaunchingWithOptions:) (or the #main struct's initializer in SwiftUI)." I notice this happened after I created an environment object.
Here is my code:
import SwiftUI
import FirebaseCore
#main
struct testagainApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var delegate
var body: some Scene {
WindowGroup {
let viewModel = AppViewModel()
ContentView()
.environmentObject(viewModel)
}
.windowStyle(HiddenTitleBarWindowStyle())
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
FirebaseApp.configure()
}
}
If I get rid of let viewModel = AppViewModel() and .environmentObject(viewModel), the simulator runs just fine. If I put the app delegate first, the simulator runs but nothing appears. I am new to Swift and am unsure how to fix this.
Option 1:
I faced same issue a while ago, then noticed that I have not linked the
GoogleService-Info.plist file to the project, if you see its already added then better remove and add it to the project via Xcode again.
Hope that solves your issue.
Option 2:
Make sure you don't have more two copies of Firebase linked into your app. If so you can remove one. See this doc on the Firebase iOS SDK for more details.
I solved it. Instead of using the AppDelegate I used the App's initializer:
struct testagainApp: App {
init() {
FirebaseApp.configure()
}
// ...
}

Can you do test setup before application loads in Swift?

I am a Swift/MacOS newbie working on a MacOS application does some setup involving keychain access and API calls in the viewDidLoad() methods in the main ViewController.
I am working on unit tests for my models, so I don't need and in fact do not want the code in viewDidLoad() to run. However, from what I can tell, the app gets loaded and those methods run before the test case setup() method, so I don't know how I could do any mocking or other actions.
I am using Xcode 11.5 and Swift 5.
One way to do it would be to have an separate NSApplicationMain for when unit tests are run vs one for "normal" runs.
First remove the #NSApplicationMain annotation from your current AppDelegate class. It should end up looking something like this:
AppDelegate.swift
import AppKit
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {
print("Debug/Production run")
// Insert code here to initialize your application
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
}
Now create a new file called AppDelegateUnitTesting.swift and it's source should look like this:
AppDelegateUnitTesting.swift
import Foundation
import Cocoa
class AppDelegateTesting: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {
print("Unit Testing Run")
// Insert code here to initialize your application
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
}
Now add a new file called main.swift this file will determine in which environment our app is running, the source should be something like this:
main.swift
import Foundation
import Cocoa
let isRunningTests = NSClassFromString("XCTestCase") != nil &&
ProcessInfo.processInfo.arguments.contains("-XCUnitTests")
fileprivate var delegate: NSApplicationDelegate?
if !isRunningTests {
delegate = AppDelegate()
NSApplication.shared.delegate = delegate
// See this Answer to initialize the Windows programmatically
// https://stackoverflow.com/a/44604229/496351
} else {
delegate = AppDelegateTesting()
NSApplication.shared.delegate = delegate
}
NSApplication.shared.run()
To determine whether it's running in a Unit Test environment it checks if it can load the XCTestClass (which is only injected when testing) and it checks for the presence of the -XCUnitTest command line argument, we have to set this argument ourselves as part of the Scheme's Test action as shown in the image below
After doing all of this, you should see the message "Debug/Production run" printed when you press the play button and you should see the message "Unit Testing Run" printed whenever you run your unit tests.
You'll most likely have to add code to load the initial window programmatically this other answer shows how to do it:
how to Load initial window controller from storyboard?

ViewController + Storyboard setting up validation with controlTextDidChange

Trying to setup validation for a few text fields in a new (and very small) Swift Mac app. Following various other topics here on SO and a few other examples, I can still not get controlTextDidChange to propagate (to my ViewController).
E.g: How to live check a NSTextField - Swift OS X
I have read at least a dozen variations of basically that same concept. Since none of the accepted answers seem to work I am just getting more and more confused by something which is generally a fairly simple task on most platforms.
I have controlTextDidChange implemented to just call NSLog to let me know if I get anything.
AppDelegate should be part of the responder chain and should eventually handle controlTextDidChange but I see nothing there either.
Using the current Xcode I start a new project. Cocoa app, Swift, Storyboard and nothing else.
From what I can gather the below isolated example should work. In my actual app I have tried some ways of inserting the ViewController into the responder chain. Some answers I found suggested it was not always there. I also tried manually adding the ViewController as the delegate in code theTextField.delegate = self
Nothing I have done seems to get text changed to trigger any events.
Any ideas why I have so much trouble setting up this delegation?
My single textfield example app
Storyboard is about as simple as it gets:
AppDelegate
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, NSTextFieldDelegate, NSTextDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
func controlTextDidChange(notification: NSNotification) {
let object = notification.object as! NSTextField
NSLog("AppDelegate::controlTextDidChange")
NSLog("field contains: \(object.stringValue)")
}
}
ViewController
import Cocoa
class ViewController: NSViewController, NSTextFieldDelegate, NSTextDelegate {
#IBOutlet var theTextField: NSTextField!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
func controlTextDidChange(notification: NSNotification) {
let object = notification.object as! NSTextField
NSLog("ViewController::controlTextDidChange")
NSLog("field contains: \(object.stringValue)")
}
}
I think the samples you're following are a bit out-of-date.
Try...
override func controlTextDidChange(_ notification: Notification) {
...as the function definition for your method in your NSTextFieldDelegate.

WatchKit didReceiveApplicationContext not being called

I can't get didReceiveApplicationContext to be called. Any ideas?
InterfaceController:
import WatchKit
import Foundation
import WatchConnectivity
class InterfaceController: WKInterfaceController, WCSessionDelegate {
#IBOutlet var colorLabel: WKInterfaceLabel!
private let session: WCSession? = WCSession.isSupported() ? WCSession.defaultSession() : nil
override init() {
super.init()
session?.delegate = self
session?.activateSession()
}
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
}
func session(session: WCSession, didReceiveApplicationContext applicationContext: [String : AnyObject]){
let colors : String = applicationContext["color"] as! String
colorLabel.setText(colors)
NSLog("session did receive application context")
}
}
I've been following along with this tutorial: http://www.kristinathai.com/watchos-2-how-to-communicate-between-devices-using-watch-connectivity/
No NSLog or setting of the colorLabel happens. No idea what I'm missing. Thanks!
This seems to be a typical 'development phase' problem!
The WCSession.defaultSession.applicationContext is buffered on the iOS device, so it is only transferred to the watch (extension) once if it doesn't change.
This lead to the strange finding, that watch extensions 'didReveiveApplicationContext:' seems not to be called, when WCSession.defaultSession.updateApplicationContext is called in the iOS app again. (Try to call WSSession.defaultSession.receivedApplicationContext in the extension to find, that the earlier transferred context is in fact available)!
In test situations, it is very helpful to add a 'changer' object to the context dictionary (like an UUID object, or - maybe even better - a NSDate.date). This will ensure, that the context has changed (compared to the buffered one) and gets transferred again (leading to a call to didReceiveApplicationContext) :-)
NSError* error = nil;
[WCSession.defaultSession updateApplicationContext:#{ #"yourKey" : #"your content",
#"forceTransfer" : NSDate.date }
error:&error];
And: Don't forget to remove this in the production version of your app, as - of course - this leads to unneeded data transfer between your app and your watch extension!
PS: The checked answer solves this problem by creating a new app. And flushing all buffers this way...
I had the same problem. In my case it helped to just close both simulators and then run the Watch scheme. This opens up both simulators again in connected state.
Hoping it helps!
I copied the above code into a new watch app and it works fine. The error must lay on the sending side. Are you certain the code in the iOS app is being called? I presume you are using Xcode and two simulators, one for the iOS app and one for the WatchApp.
The code on the iOS side is not run unless you open the app on the phone simulator. Where and how on the iOS side are you issuing the updateAppContext call?
In my test, this is all that I added to my ViewController.swift on the iOS side (This code will not be triggered until I start the iOS app on my iPhone.)
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
do {
try session.updateApplicationContext( ["color" : "Red" ])
} catch _ {
}
}
For me, when doing some testing/debugging i tried to fire updateApplicationContext in AppDelegate. This caused the didReceiveApplicationContext not being called.
Moving this logic to a later point, like in a UIViewController made it work for me at least.
Check the session to make sure its isPaired and watchAppInstalled properties are both YES. It seems like updating the shared context while these are NO will not work.
I was having this issue. Made a change to not update the context when either condition was NO. Added an implementation of sessionWatchStateDidChange:, and if both conditions were YES, updated the context. It worked.
I suspect this in combination with another issue where the phone will not send the context if the data is not different causes the "never updating" issue. A workaround of passing a "uuid" did help but I suspect the above is a better fix.
In my case, I used the following code to send my application context:
do {
try session.updateApplicationContext(applicationContext)
} catch let error {
throw error
}
and neither didReceiveApplicationContext was called, nor an error was thrown.
My problem was that applicationContext contained a custom object, whereas only property list items are allowed.
The strange point is that no error was thrown.

Swift NSUserNotification doesn't show while app is active

I am new to OSX development and I am making an app which fires a notification when something happens. But it isn't showing the notification when the app is the key app, as it is the default behavior. I want to show them even when the app IS the key app.
However I only found solutions to this matter that were written in objective-c but right now I am working with Swift. I was wondering how I could I implement it with Swift.
To ensure the notifications are always shown you'll need to set a delegate for NSUserNotificationCenter and implement userNotificationCenter(center:shouldPresentNotification:) -> Bool. The documentation says this message is
Sent to the delegate when the user notification center has decided not
to present your notification.
You can implement the delegate in any class of your choosing. Here is an example:
class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDelegate {
func applicationDidFinishLaunching(aNotification: NSNotification) {
NSUserNotificationCenter.defaultUserNotificationCenter().delegate = self
}
func userNotificationCenter(center: NSUserNotificationCenter, shouldPresentNotification notification: NSUserNotification) -> Bool {
return true
}