SwiftUI: Access #EnvironmentObject from AppDelegate - swift

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

Related

Cast AppDelegate in SwiftUI Lifecycle

How do you do the cast below, in an App with SwiftUI Lifecycle, where AppDelegate is implemented as NSApplicationDelegateAdaptor?
let appDelegate = NSApplication.shared.delegate as! AppDelegate
// or
let appDelegate = NSApp.delegate as! AppDelegate
The above throws the following error:
Could not cast value of type 'SwiftUI.AppDelegate' (0x1e28fae40) to 'MyApp.AppDelegate' (0x102d277c0).
Background is using AppDelegate in an Extension
extension NSStatusBarButton {
public override func mouseDown(with event: NSEvent) {
super.mouseDown(with: event)
let appDelegate = NSApp.delegate as! AppDelegate
// do stuff with appDelegate on MouseDown Event
}
}
We must not cast, because NSApp's delegate is not our delegate, but internal, which redirects some callbacks to that one injected via adapter.
We can access our delegate directly via variable of adapter:
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
or via EnvironmentObject (for that AppDelegate have confirm to ObservableObject protocol), which is injected into ContentView for us automatically and so available inside all subviews:
struct MyView: View {
#EnvironmentObject var appDelegate: AppDelegate
// ...

Environment Object Method error in Scene Delegate

SO Community
I have been self-teaching myself SwiftUI and I've just come across a problem while creating an application in the Scene Delegate. When I add an environment object method to the scene delegate to assign an object to the environment of a view hierarchy, Xcode sends these errors on my let contentView = ContentView().environmentObject(delegate.myData) line of code, "Expected member name following '.' ", and "Missing argument for parameter 'appData' in call ".
I will also attach a picture of my ContentView.swift File to show where I placed my #EnviromentObject reference within the body Content View. If you guys have any questions can you please try to help me out, this has been a bum time. Thanks.
Link to Scene Delegate.Swift file : enter image description here
Link to ContentView.Swift file : enter image description here
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options
connectionOptions: UIScene.ConnectionOptions) {
let app = UIApplication.shared
let delegate = app.delegate as! AppDelegate
let contentView = ContentView()
.environmentObject(delegate.myData)
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
EnvironmentObjects are not passed via parameter like you're trying to do in your ContentView initializer. Instead, they are passed "magically" behind-the-scenes and exposed via the #EnvironomentObject property wrapper.
My suggestion is to get rid of your init method and use onAppear { } to set your contentData fields:
var body: some View {
VStack {
//...
}.onAppear {
// do initial setup
}
}
You should also make sure that you're using the same capitalization of appData -- you're using appdata sometimes, which is leading to many of your compilation errors.

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

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

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

Swift: How to access in AppDelegate variable from the View controller?

I would like to write in the text or csv (prefer) file the variable from the view controller.
Indeed I am doing a drawing app and I would like to write in the file the current position of the finger.
class ViewController: UIViewController {var lastPoint = CGPoint.zeroPoint ... }
I would like to have access to lastPoint.x and lastPoint.y from the AppDelegate. how I could do that ? Thank you.
Your question is full of confusion but if that's what you are looking for:
You can access the appDelegate by getting a reference to it like that:
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
after that if you have store a property called lastPoint in your appDelegate you can access its components very simply like that:
let x = appDelegate.lastPoint.x
let y = appDelegate.lastPoint.y
If you need to access your viewController properties from the AppDelegate, then I suggest having a reference to your view controller in your appdelegate:
var myViewController: ViewController!
then when your view controller is created you can store a reference to it in the appdelegate property:
If your create your view controller outside of your appDelegate:
Swift 1-2 syntax
var theViewController = ViewController()
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.myViewController = theViewController
Swift 3-4 syntax
var theViewController = ViewController()
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.myViewController = theViewController
If you create your view controller inside of your appDelegate:
self.myViewController = ViewController()
After that you can access your data from your viewcontroller from your appdelegate just by accessing its property like that:
let x = self.myViewController.lastPoint.x
let y = self.myViewController.lastPoint.y
Swift 3 Update
var theViewController = ViewController()
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.myViewController = theViewController
You can create a BaseViewController and write this
class BaseViewController {
lazy var appDelegate : AppDelegate {
return UIApplication.shared.delegate as? AppDelegate
}
}
and inherit other viewcontrollers with BaseViewController and access this by
class ViewController : BaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
print(self.appDelegate?.lastPoint.x)
print(self.appDelegate?.lastPoint.x)
}}
I did manage to access my ViewController from the App delegate by creating an instance of ViewController in AppDelegate.swift, like:
var mainViewController = ViewController()
func applicationWillEnterForeground(_ application: UIApplication) {
mainViewController.myVariable = "Hello".
}
Still, I don't understand how does the AppDelegate "know" that mainViewController is supposed to point to that particular ViewController. As my application has a single view and a single ViewController, that's fine, but what it I had multiple ViewControllers associated with different UIViews? Appreciate if anyone here can shed a light into that.
Best Regards,
Andre