Is there a main.swift which is equivalent to the #NSApplicationMain annotation? - swift

Creating a new Cocoa project in XCode gives me an AppDelegate.swift file which looks like this:
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var window: NSWindow!
}
The #NSApplicationMain attribute is documented here as
NSApplicationMain
Apply this attribute to a class to indicate that it is the application delegate. Using this attribute is equivalent to calling the NSApplicationMain(_:_:) function.
If you do not use this attribute, supply a main.swift file with code at the top level that calls the NSApplicationMain(_:_:) function as follows:
import AppKit
NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
The instructions in the documentation do not work: the AppDelegate class is never instantiated. In this answer, vadian suggests the following contents for main.swift, which work better than the code in the documentation:
import Cocoa
let appDelegate = AppDelegate()
NSApplication.shared().delegate = appDelegate
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
However, this still does not provide the same behavior as #NSApplicationMain. Consider using the above main.swift with the following AppDelegate.swift:
import Cocoa
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var window: NSWindow!
var foo: NSStatusBar! = NSStatusBar.system();
}
The above AppDelegate.swift works with an #NSApplicationMain annotation, but when using the above main.swift, it fails at runtime with the error
Assertion failed: (CGAtomicGet(&is_initialized)), function CGSConnectionByID, file Services/Connection/CGSConnection.c, line 127.
I think this is_initialized error means that #NSApplicationMain sets things up so that the AppDelegate is instantiated after some initialization by the NSApplicationMain function. This suggests the following main.swift, which moves the delegate initialization to after the NSApplicationMain call:
import Cocoa
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
let appDelegate = AppDelegate()
NSApplication.shared().delegate = appDelegate
However, this doesn't work either, because NSApplicationMain never returns! The above main.swift is equivalent to the broken suggestion in the documentation, because the latter two lines are dead code.
I therefore think there must be some way to pass a reference to my AppDelegate class as an argument to the NSApplicationMain function, so that Cocoa can do its initialization and then instantiate my AppDelegate class itself. However, I see no way to do this.
Is there a main.swift which provides behavior which is truly equivalent to the #NSApplicationMain annotation? If so, what does that main.swift look like? If not, what is #NSApplicationMain actually doing, and how do I modify it?

The documentation assumes that there is a xib or storyboard which instantiates the AppDelegate class via an object (blue cube) in Interface Builder. In this case both
main.swift containing NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
and
#NSApplicationMain in the AppDelegate class
behave exactly the same.
If there is no xib or storyboard you are responsible to initialize the AppDelegate class, assign it to NSApplication.shared.delegate and run the app. You have also to consider the order of appearance of the objects. For example you cannot initialize objects related to AppKit before calling NSApplication.shared to launch the app.
For example with this slightly changed syntax
let app = NSApplication.shared
let appDelegate = AppDelegate()
app.delegate = appDelegate
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
you can initialize the status bar in AppDelegate outside ofapplicationDidFinishLaunching:
let statusItem = NSStatusBar.system().statusItem(withLength: -1)
because NSApplication.shared() to launch the app is called before initializing the AppDelegate class.

Here is what I did in order to run application without #NSApplicationMain annotation and function NSApplicationMain(_, _) while using Storyboard with initial NSWindowController generated by Xcode application template (with slight modification related to Main Menu described below).
File: AppConfig.swift (Swift 4)
struct AppConfig {
static var applicationClass: NSApplication.Type {
guard let principalClassName = Bundle.main.infoDictionary?["NSPrincipalClass"] as? String else {
fatalError("Seems like `NSPrincipalClass` is missed in `Info.plist` file.")
}
guard let principalClass = NSClassFromString(principalClassName) as? NSApplication.Type else {
fatalError("Unable to create `NSApplication` class for `\(principalClassName)`")
}
return principalClass
}
static var mainStoryboard: NSStoryboard {
guard let mainStoryboardName = Bundle.main.infoDictionary?["NSMainStoryboardFile"] as? String else {
fatalError("Seems like `NSMainStoryboardFile` is missed in `Info.plist` file.")
}
let storyboard = NSStoryboard(name: NSStoryboard.Name(mainStoryboardName), bundle: Bundle.main)
return storyboard
}
static var mainMenu: NSNib {
guard let nib = NSNib(nibNamed: NSNib.Name("MainMenu"), bundle: Bundle.main) else {
fatalError("Resource `MainMenu.xib` is not found in the bundle `\(Bundle.main.bundlePath)`")
}
return nib
}
static var mainWindowController: NSWindowController {
guard let wc = mainStoryboard.instantiateInitialController() as? NSWindowController else {
fatalError("Initial controller is not `NSWindowController` in storyboard `\(mainStoryboard)`")
}
return wc
}
}
File main.swift (Swift 4)
// Making NSApplication instance from `NSPrincipalClass` defined in `Info.plist`
let app = AppConfig.applicationClass.shared
// Configuring application as a regular (appearing in Dock and possibly having UI)
app.setActivationPolicy(.regular)
// Loading application menu from `MainMenu.xib` file.
// This will also assign property `NSApplication.mainMenu`.
AppConfig.mainMenu.instantiate(withOwner: app, topLevelObjects: nil)
// Loading initial window controller from `NSMainStoryboardFile` defined in `Info.plist`.
// Initial window accessible via property NSWindowController.window
let windowController = AppConfig.mainWindowController
windowController.window?.makeKeyAndOrderFront(nil)
app.activate(ignoringOtherApps: true)
app.run()
Note regarding MainMenu.xib file:
Xcode application template creates storyboard with Application Scene which contains Main Menu. At the moment seems there is no way programmatically load Main Menu from Application Scene. But there is Xcode file template Main Menu, which creates MainMenu.xib file, which we can load programmatically.

Replace the default Cocoa project's AppDelegate.swift with the following main.swift. The application will behave the same as before. Thus, the following code provides the semantics of the #NSApplicationMain annotation.
import Cocoa
class AppDelegate: NSObject, NSApplicationDelegate { }
let myApp: NSApplication = NSApplication.shared()
let myDelegate: AppDelegate = AppDelegate()
myApp.delegate = myDelegate
let mainBundle: Bundle = Bundle.main
let mainNibFileBaseName: String = mainBundle.infoDictionary!["NSMainNibFile"] as! String
mainBundle.loadNibNamed(mainNibFileBaseName, owner: myApp, topLevelObjects: nil)
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
(I constructed this with much help from vadian's answer. If there are any differences in behavior between the above and the default Cocoa project application, please let me know.)

Related

How can I update my Window via AppDelegate in macOS Cocoa?

I am using ViewController for my app content, I want be able to update my app window in AppDelegate. I know that I can update my window from ViewController but for this question I want update my window from AppDelegate while ViewController is going take care of content of my app.
import Cocoa
#main
class AppDelegate: NSObject, NSApplicationDelegate {
let window: NSWindow? = NSApp.mainWindow
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
window?.center()
window?.setFrameAutosaveName("Main Window")
window?.title = "changed label"
window?.styleMask.remove(.closable)
window?.styleMask.remove(.fullScreen)
window?.styleMask.remove(.miniaturizable)
window?.makeKeyAndOrderFront(nil)
}
}
Add a declaration for your view controller and then add it to the window; (example)
var myVC = aViewController()
inside applicationDidFinishLaunching;
window?.contentView?.addSubview(aVC.view)

WatchKit App Get's Nil when initializing class

I am trying to update my app to utilize SwiftUI architecture. It is a stand alone WatchKit app. I used to pass a few key classes between views utilizing the delegate approach. Since I am trying to utilize environmentObject, I would like to initialize the initial classes (which depend on each other) via the delegate.
Given I am using SwiftUI method, I have recreated AppDelegate in the #main.
import SwiftUI
class AppDelegate: NSObject, WKExtensionDelegate {
var class1: Class1?
var class2: Class2! = Class2()
var class3: Class3!
func application(_ application: WKExtension) -> Bool {
return true
}
}
#main
struct WatchApp: App {
#WKExtensionDelegateAdaptor(AppDelegate.self) var delegate
init() {
delegate.class1 = Class1()
delegate.class2 = Class2()
delegate.class3 = Class3()
}
#SceneBuilder var body: some Scene {
WindowGroup {
NavigationView {
ContentView()
.environmentObject(delegate.class3)
.environmentObject(delegate.class2)
.environmentObject(delegate.class1)
}
}
WKNotificationScene(controller: NotificationController.self, category: "myCategory")
}
}
When Class 3 get's called I get a nil value in the access of the AppDelegate and a crash.
#if os(macOS)
let delegate = NSApplication.shared.delegate as! AppDelegate
#elseif !os(watchOS)
let delegate = UIApplication.shared.delegate as! AppDelegate
#else
let delegate = WKExtension.shared().delegate as! AppDelegate //<HERE's The Crash - Thread 1:EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
#endif
}
And in the info.plist
<key>WKExtensionDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).AppDelegate</string>
Is there an obvious thing I am overlooking?
WKExtension.shared() is always defined, but delegate property may be nil. Using as! is what crashes your app
You will have to provide a delegate to handle lifecycle events in your extension, see Apple Doc
To assign an AppDelegate for the extension, follow this steps:
Create a class with name YOUR_CLASS_ExtensionDelegate that implements the protocol WKExtensionDelegate.
Make sure the value of WKExtensionDelegateClassName in Info.plist in WatchKit Extension is $(PRODUCT_MODULE_NAME).YOUR_CLASS_ExtensionDelegate
In your case, you already did 1, but you should check for 2

SwiftUI: Access #EnvironmentObject from AppDelegate

I want to use the applicationWillTerminate function to save some user defaults before the app closes. The data I want to save is stored in an EnvironmentObject.
How can I access it from the AppDelegate class?
An #EnvironmentObject doesn't need to be instantiated directly in your SwiftUI objects; rather, it can be allocated somewhere else (for example, your UISceneDelegate) and then passed through using the .environment(…) function.
You could also allocate it on your AppDelegate, and pass that object though to your views in UISceneDelegate.scene(_:willConectTo:options:) method.
Paul Hudson has a good description of all of this at https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-environmentobject-to-share-data-between-views
In case someone needs code example for this answer.
1.Create class that conforms to ObservableObject
class Test: ObservableObject{ }
2.in AppDelegate.Swift declare var myVar = Test()
class AppDelegate: UIResponder, UIApplicationDelegate {
var myVar = Test()
//****
}
3.in SceneDelegate.swift in "if let windowScene = scene as? UIWindowScene {" change the code like this :
if let windowScene = scene as? UIWindowScene {
let myVar = (UIApplication.shared.delegate as! AppDelegate).myVar
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(myVar))
self.window = window
window.makeKeyAndVisible()
}

Swift 4 Referencing a UITextField programatically

Relatively new to coding in Swift and am well on my way to my first App, however, this issue is totally foxing me and I cannot work out why.
Why can I not reference the InputTextField.text property?
Playground image
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
override func loadView() {
setupInputView()
}
func setupInputView () {
let InputTextField = UITextField()
InputTextField.text = "Some Text"
view.addSubview(InputTextField)
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
print("InputTextField: \(InputTextField.text)")
InputTextField has been declared inside the function setupInputView(). It is only accessible inside that function. Move the declaration
let InputTextField = UITextField()
outside the function. It will then be a member of your class MyViewController. Outside of that class you can access as follows
PlaygroundPage.current.liveView.InputTextField
Go read up on scope and variable declarations before you continue
The problem with your code is, you are setting up the textField inside a function. So the textField variable has a local scope and it's become inaccessible with myViewController instance. So first of all, you have to make the textField as a global variable. Please see the code snippet below-
class MyViewController : UIViewController {
let inputTextField = UITextField()
override func loadView() {
inputTextField.text = "some text"
}
}
let myViewController = MyViewController()
PlaygroundPage.current.liveView = myViewController
print("TextFieldValue: \(myViewController.inputTextField.text!)")
Please remember one thing about the variable naming convention, it's always start with small letter and then follow camelCasing.

Xcode: how to create instances of views and pass info to them?

I'm trying to create a MacOS app that plays audio or video files. I've followed the simple instructions on Apple's website here
But I want to use the File > Open menu items to bring up an NSOpenPanel, and pass that to the View Controller.
So presumably, the Open action should be in the AppDelegate, as the ViewController window might not be open.
And then pass the filename to a new instance of the ViewController window.
Is that right? If so, how do I "call" the View from AppDelegate?
Here's the AppDelegate:
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
#IBAction func browseFile(sender: AnyObject) {
let dialog = NSOpenPanel();
if (dialog.runModal() == NSModalResponseOK) {
let result = dialog.url // Pathname of the file
if (result != nil) {
// Pass the filepath to the window view thing.
} else {
// User clicked on "Cancel"
return
}
}
}
and here's the ViewController:
class ViewController: NSViewController {
#IBOutlet weak var playerView: AVPlayerView!
override func viewDidLoad() {
super.viewDidLoad()
// Get the URL somehow
let player = AVPlayer(url: url)
playerView.player = player
}
There are some details not disclosed in your question, but I believe I can provide the proper answer still.
You can call NSOpenPanel from AppDelegate, nothing wrong with that. Just note that user may cancel the dialog and how to handle that situation.
Considering the view the best thing is to create WindowController that is connected to the ViewController (it is like that by default) in the Storyboard, then access it from the code using NSStoryBoard.instantiateController(withIdentifier:), and then use its window property with something like window.makeKeyAndOrderFront(self) . If you have NSWindow or NSWindowController class in your code then you should initialize the class in the code and again make window key and front.