Can you do test setup before application loads in Swift? - 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?

Related

Swift Command Line Tool Not Receiving DistributedNotificationCenter Notifications

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

Action of StatusItem not working in Swift

So I am a newbie to Swift and wanted to create a simple example status bar app on MacOS.
To keep things clean I created a subclass App which is creating the status item. This class is then created in the applicationDidFinishLaunching function of the AppDelegate.swift.
But somehow nothing is printed on the console when I press the status icon. However if I copy the code in the AppDelegate file it works. Does someone know what I am doing wrong and why it is not working in the subclass?
Here is the code of my own class:
import Cocoa
class App: NSObject {
let menuBarItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
override init() {
print("created app instance");
if let button = menuBarItem.button {
button.image = NSImage(named: NSImage.Name("StatusBarButtonImage"))
button.action = #selector(test(_:))
}
}
#objc func test(_ sender: Any?) {
print("button was pressed")
}
}
and the AppDelegate:
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var appInstance: App!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
appInstance = App()
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
}
If the button is showing up and nothing is happening when you click it, it looks to me like you need to make sure you're setting your button's target to your App instance. E.g.:
button.target = self
Otherwise the action is only followed up the responder chain.

Activate Application on Screen Unlock

I'm writing an application for macOS and I want it to detect when the screen is unlocked and then make itself become the active application.
I'm trying to use "com.apple.screenIsUnlocked", but it doesn't seem to work (the function doesn't even run). I also tried using NSWorkspaceDidWakeNotification where I got the function to run but the app didn't actually activate (presumably because the screen was still locked).
Here is what I currently have (I'm using Xcode 9.2 and Swift 4):
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(AppDelegate.screenDidUnlock), name: NSNotification.Name(rawValue: "com.apple.screenIsUnlocked"), object: nil)
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
#objc func screenDidUnlock() {
NSApplication.shared.activate(ignoringOtherApps: true)
print("Did Run")
}
The com.apple.screenIsUnlocked notification is posted to the DistributedNotificationCenter rather than the NSWorkspace's notificationCenter, so the observer should be added like this:
DistributedNotificationCenter.default().addObserver(self, selector: #selector(AppDelegate.screenDidUnlock), name: NSNotification.Name(rawValue: "com.apple.screenIsUnlocked"), object: nil)enter code here
I don't think apple allows you to run any application in the background.

Understanding Retain count in an AppDelegate

Within an application, I'm wondering why an instance of a class's deinit method is not being called when quitting the application.
As an example, the Test class presented here is created in the AppDelegate's applicationDidFinishLaunching.
import Cocoa
class Test {
let testVar = 1
init() {
print("Retain count \(CFGetRetainCount(self))")
NSApplication.shared().terminate(self)
}
deinit {
print("Calling deinit")
}
}
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
//#IBOutlet weak var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
_ = Test()
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
print("Terminating")
}
}
Not only does this fail to call Test's deinit method, but the retain count in Test's init is 2; I would have expected this to be 1.
If an optional reference is stored in the AppDelegate class and set when creating the Test instance, it is nil when applicationWillTerminate is called
Can someone please explain why the retain count is 2 here and how to ensure that deinit of Test is called when the application is terminated?
I assume the Swift situation is the same as the Objective-C one documented at this link:
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmRules.html#//apple_ref/doc/uid/20000994-BAJHFBGH
"When an application terminates, objects may not be sent a dealloc message. Because the process’s memory is automatically cleared on exit, it is more efficient simply to allow the operating system to clean up resources than to invoke all the memory management methods."
I can't speak to why the retain count is 2. In general, with ARC enabled, you really shouldn't ever inspect the retain count, and the reason why is a well answered question
Additionally, there is an answer that suggests CFGetRetainCount might actually be increasing the retain count when you call it.
In your situation, deinit is not being called because you're terminating the app programmatically before the initializer is finished.
In the situation of having an instance of Test assigned to a variable on your AppDelegate, deinit is not called because AppDelegate is not released in the "normal" way when an app exits. Therefore, all of it's properties are not released. If you set the variable to nil in applicationWillTerminate, you'll see that deinit is then called appropriately. The for this behavior is explained when talking about global variables in this answer.
To provide a specific permutation of your provided example:
class Test {
let testVar = 1
init() {
print("Retain count \(CFGetRetainCount(self))")
}
deinit {
print("Calling deinit")
}
}
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var variable: Test?
func applicationDidFinishLaunching(_ aNotification: Notification) {
variable = Test()
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
// If you're trying to pass in `variable`, the terminate funciton will retain it
NSApplication.shared.terminate(nil)
}
}
func applicationWillTerminate(_ aNotification: Notification) {
variable = nil
}
}
deinit is only guaranteed to be called if the instance is release by ARC, but if release is never called, for instance, if the app crashes or is force quite by the user it won't be So, don't rely on it for absolutely critical "clean up".
Circling back on the retain count. Your code, executed exactly as is in your question, produces the following:
What we're seeing is that the retain count being incremented by one during the call to CFGetRetainCount, then decremented when it returns, then incremented again once more when it's passed to terminate.
The issue is due to terminating the application from within the init of the Test class. I suspect that calling terminate in the init is preventing the correct instantiation of the class, so its deinit is never called.
By delaying the call to terminate, the call to Test's deinit is called, as expected
import Cocoa
class Test {
init() {
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
NSApplication.shared().terminate(self)
}
}
deinit {
print ("Calling Deinit")
}
}
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
//#IBOutlet weak var window: NSWindow!
var variable: Test?
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
variable = Test()
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
variable = nil
print("Terminating")
}
}

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.