Now that AppDelegate and SceneDelegate are removed from SwiftUI, where do I put the code that I used to have in SceneDelegate and AppDelegate, Firebase config for ex?
So I have this code currently in my AppDelegate:
Where should I put this code now?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
FirebaseConfiguration.shared.setLoggerLevel(.min)
FirebaseApp.configure()
return true
}
Here is a solution for SwiftUI life-cycle. Tested with Xcode 12b / iOS 14
import SwiftUI
import UIKit
// no changes in your AppDelegate class
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print(">> your code here !!")
return true
}
}
#main
struct Testing_SwiftUI2App: App {
// inject into SwiftUI life-cycle via adaptor !!!
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Overriding the initializer in your App also works:
import SwiftUI
import Firebase
#main
struct BookSpineApp: App {
init() {
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
BooksListView()
}
}
}
Find a more detailed write-up here:
The Ultimate Guide to the SwiftUI 2 Application Life Cycle
Firebase and the new SwiftUI 2 Application Life Cycle
You should not put that kind of codes in the app delegate at all or you will end up facing the Massive App Delegate. Instead, you should consider refactoring your code to more meaningful pieces and then put the right part in the right place. For this case, the only thing you need is to be sure that the code is executing those functions once the app is ready and only once. So the init method could be great:
#main
struct MyApp: App {
init() {
setupFirebase()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
private extension MyApp {
func setupFirebase() {
FirebaseConfiguration.shared.setLoggerLevel(.min)
FirebaseApp.configure()
}
}
AppDelegate ?
You can have your own custom class and assign it as the delegate. But note that it will not work for events that happen before assignment. For example:
class CustomDelegate: NSObject, UIApplicationDelegate {
static let Shared = CustomDelegate()
}
And later:
UIApplication.shared.delegate = CustomDelegate.Shared
Observing For Notifications
Most of AppDelegate methods are actually observing on notifications that you can observe manually instead of defining a new class. For example:
NotificationCenter.default.addObserver(
self,
selector: #selector(<#T###objc method#>),
name: UIApplication.didBecomeActiveNotification,
object: nil
)
Native AppDelegate Wrapper
You can directly inject app delegate into the #main struct:
#UIApplicationDelegateAdaptor(CustomDelegate.self) var appDelegate
Note: Using AppDelegate
Remember that adding AppDelegate means that you are killing default multiplatform support and you have to check for platform manually.
You can also use the new ScenePhase for certain code that the AppDelegate and SceneDelegate had. Like going to the background or becoming active. From
struct PodcastScene: Scene {
#Environment(\.scenePhase) private var phase
var body: some Scene {
WindowGroup {
TabView {
LibraryView()
DiscoverView()
SearchView()
}
}
.onChange(of: phase) { newPhase in
switch newPhase {
case .active:
// App became active
case .inactive:
// App became inactive
case .background:
// App is running in the background
#unknown default:
// Fallback for future cases
}
}
}
}
Example credit: https://wwdcbysundell.com/2020/building-entire-apps-with-swiftui/
Note the method below will stop cross platform support so should only be used if you are planning on building for iOS only.
It should also be noted that this doesn’t use the SwiftUI lifecycle method, instead it allows you to return to the UIKit lifecycle method.
You can still have an AppDelegate and a SceneDelegate when you create a SwiftUI app in Xcode 12-beta.
You just need to make sure that you have chosen the correct option for the Life Cycle when you create your app.
Make sure you choose UIKit App Delegate for the Life Cycle and you will get an AppDelegate and a SceneDelegate
I would also advise in using the main App's init method for this one, as it seems safe to use (any objections?).
What I usually do, that might be useful to share, is to have a couple of utility types, combined with the Builder pattern.
/// An abstraction for a predefined set of functionality,
/// aimed to be ran once, at app startup.
protocol StartupProcess {
func run()
}
/// A convenience type used for running StartupProcesses.
/// Uses the Builder pattern for some coding eye candy.
final class StartupProcessService {
init() { }
/// Executes the passed-in StartupProcess by running it's "run()" method.
/// - Parameter process: A StartupProcess instance, to be initiated.
/// - Returns: Returns "self", as a means to chain invocations of StartupProcess instances.
#discardableResult
func execute(process: any StartupProcess) -> StartupProcessService {
process.run()
return self
}
}
and then we have some processes
struct CrashlyticsProcess: StartupProcess {
func run() {
// Do stuff, like SDK initialization, etc.
}
}
struct FirebaseProcess: StartupProcess {
func run() {
// Do stuff, like SDK initialization, etc.
}
}
struct AppearanceCustomizationProcess: StartupProcess {
func run() {
// Do stuff, like SDK initialization, etc.
}
}
and finally, running them
#main
struct TheApp: App {
init() {
initiateStartupProcesses()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
private extension TheApp {
func initiateStartupProcesses() {
StartupProcessService()
.execute(process: ExampleProcess())
.execute(process: FirebaseProcess())
.execute(process: AppearanceCustomizationProcess)
}
}
Seems quite nice and super clean.
I see a lot of solutions where init gets used as didFinishLaunching. However, didFinishLaunching gets called AFTER init of the App struct.
Solution 1
Use the init of the View that is created in the App struct. When the body of the App struct gets called, didFinishLaunching just happened.
#main
struct MyApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#ViewBuilder
var body: some Scene {
WindowGroup {
MainView(appDelegate: appDelegate)
}
}
}
struct MainView: View {
init(appDelegate: AppDelegate) {
// at this point `didFinishLaunching` is completed
setup()
}
}
Solution 2
We can create a block to notify us when didFinishLaunching gets called. This allows to keep more code in SwiftUI world (rather than in AppDelegate).
class AppDelegate: NSObject, UIApplicationDelegate {
var didFinishLaunching: ((AppDelegate) -> Void)?
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions
launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil
) -> Bool {
didFinishLaunching?(self)
return true
}
}
#main
struct MyApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#ObservedObject private var applicationModel = ApplicationModel()
// `init` gets called BEFORE `didFinishLaunchingWithOptions`
init() {
// Subscribe to get a `didFinishLaunching` call
appDelegate.didFinishLaunching = { [weak applicationObject] appDelegate in
// Setup any application code...
applicationModel?.setup()
}
}
var body: some Scene {
return WindowGroup {
if applicationObject.isUserLoggedIn {
LoggedInView()
} else {
LoggedOutView()
}
}
}
}
Related
I'm working on integrating a custom iOS plugin into my Flutter app, problem is that I'm not getting delegate callbacks from the custom SDK Protocol.
I have to connect a bluetooth device to my app and I from the delegate calls I should receive the device's ID and pair it.
From the Flutter side, I can call the native functions from the customSdk: sdkInstance.scan() and there are even some internal (inside the sdk) prints with the scan results but my delegate calls are not in place.
I think I'm not correctly adding the delegate to the SDK, I can get this to work in a swift native app but not as a Flutter Plugin.
So here's more or less the code:
iOS Code
AppDelegate.swift
import UIKit
import Flutter
#UIApplicationMain
#objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
SwiftIosPlugin.swift
import Flutter
import UIKit
import CustomSDK
public class SwiftIosPlugin: NSObject, FlutterPlugin {
let sdkInstance = CustomSDK.shared // This returns an instance of the SDK
let channel: FlutterMethodChannel
public static func register(with registrar: FlutterPluginRegistrar)
let channel = FlutterMethodChannel(name: "ios_plugin_channel", binaryMessenger: registrar.messenger())
let instance = SwiftIosPlugin(channel)
registrar.addMethodCallDelegate(instance, channel: channel)
registrar.addApplicationDelegate(instance)
}
init (_ channel: FlutterMethodChannel) {
self.channel = channel
super.init()
// In Swift, this is done in viewDidLoad()
// Is this the correct place to do this?
sdkInstance.addDelegate(self)
}
public func handle(_ call: FlutterMethodCall, result: #escaping FlutterResult) {
switch call.method {
case "startScan":
do {
// This is being called and results printed
try sdkInstance.scan()
} catch {
result(FlutterError(code: "400", message: "\(error)", details: nil))
}
case "connect":
sdkInstance.connect(call, result)
default:
result(FlutterMethodNotImplemented)
}
}
}
// These should be called but are not
extension SwiftIosPlugin: CustomSDKDelegate {
// Isn't called when scan() is executeed!
public func onScanDevice(didScan id:String) {
// do logic
}
public func onPairedDevice(didPair id:String) {
// do logic
}
}
Update:
Silly thing that I hope nobody else has this trouble...
Two things to consider:
The problem was some of the delegate's functions public func onScanDevice(didScan id:String) was missing a parameter (even though there weren't any errors pointed out by Xcode).
sdkInstance.addDelegate(self) was called too early in the class "lifecycle".
Be mindful of these things and you won't have any trouble!
Hello I have been following (https://firebase.google.com/docs/auth/ios/google-signin) to setup a simple Google SignIn with Firebase in an iOS app. I use Xcode 12.5.1. I created a new Storyboard (UIKit) app. I did pod init and added Firebase and GoogleSignIn and did pod install.
I'm also total noob in iOS.
I'm trying to follow the steps and for non of the steps it really explains where to put the code. For some I think I succeeded, but for the step 4. I have no idea where to put that code. I tried looking for other posts/tutorials for setting up GoogleSignIn with Firebase and SwiftUI and non of them work. Steps seem pretty simple, but they probably omit something very simple that a noob in iOS doesn't know.
Can some1 please explain a bit this step 4 at least.
Some of you commented to put it in the AppDelegate. I have created AppDelegate, but not sure where to put it there. Creating a signIn function in AppDelegate for dumping the code from the step 4 does not work. I also tried to wrap it with UIViewControllerRepresentable like here with the same problem.
It complains that this presenting: self from GIDSignIn.sharedInstance.signIn(with: config, presenting: self) { [unowned self] user, error in (code from step 4 that I don't know where to put) is AppDelegate that can't be converted to UIViewController that is expected.
import SwiftUI
import Firebase
import GoogleSignIn
#main
struct FolderlesApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
return true
}
#available(iOS 9.0, *)
func application(_ application: UIApplication, open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any])
-> Bool {
return GIDSignIn.sharedInstance.handle(url)
}
}
I think you are using SwfitUI not UIkit in the code you provided. If this is true. I can answer the SwiftUI solution.
Assume you have already set up the Step 1 (URL Types) and import the GoogleService-Info.plist. If you don’t, make sure to do it first.
Then, we have to create AppDelegate by ourselves in SwiftUI. You can add this code into your “App” type struct file (usually, the file name is your Project_NameApp.swift). Just like the example down below. This is step 2, step 3 in Authenticate Using Google Sign-In on iOS | Firebase
// Project_NameApp.swift
struct Project_NameApp: App {
// add this var define line
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
TestLoginView()
}
}
}
// add this class
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
// add this function for init FirebassApp
FirebaseApp.configure()
return true
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
// add this function for Google Sign in to handle the url
return GIDSignIn.sharedInstance.handle(url)
}
}
The next one is your main question - Where the step 4’s code should we be put? Following code is the example SwiftUI view that you want to login with google. You can see it just turn the step 4 code into the function, and bind to the Button you want the user to tap and Sign in with a Google account.
import SwiftUI
import GoogleSignIn
import Firebase
struct TestLoginView: View {
var body: some View {
VStack {
Button(action: {
// Just add the action function, it will works.
googleSignIn()
}, label: {
Text("Google Sign In")
})
}
}
func googleSignIn(){
guard let clientID = FirebaseApp.app()?.options.clientID else { return }
// Create Google Sign In configuration object.
let config = GIDConfiguration(clientID: clientID)
// Start the sign in flow!
GIDSignIn.sharedInstance.signIn(with: config, presenting: (UIApplication.shared.windows.first?.rootViewController)!) { user, error in
if let error = error {
print(error.localizedDescription)
return
}
guard
let authentication = user?.authentication,
let idToken = authentication.idToken
else {
return
}
let credential = GoogleAuthProvider.credential(withIDToken: idToken,
accessToken: authentication.accessToken)
// Authenticate with Firebase using the credential object
Auth.auth().signIn(with: credential) { (authResult, error) in
if let error = error {
print("authentication error \(error.localizedDescription)")
return
}
print(authResult ?? "none")
}
}
}
}
struct TestLoginView_Previews: PreviewProvider {
static var previews: some View {
TestLoginView()
}
}
I have tested it on my Xcode and it works for me. You can try it and hope you can solve the problem!
Tips 1: This is just a simple code. If you want a better implementation. I recommend you implement with MVVM structure. In this case, you should put the function into your ViewModel which you create by yourself. The button should call the Google Sign in function in your ViewModel.
Tips 2: This code doesn’t implement the multi-factor login with phone verify, check out this article for more detail. Most use case won’t use this less frequently used multi-factor login.
I am currently struggling to figure out where to correctly place the .register() method to set an initial/default value for UserDefaults (on every launch of the app).
Here is where I tried to initialize it in the "App" file that Xcode generated with the project:
import SwiftUI
#main
struct TestApp: App {
init() {
//Sets default values for the user defaults that have not yet been set and should not return 0/false
UserDefaults.standard.register(defaults: [
"selectedRoundLength": 1
]
)
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
However, this doesn't really seem to do the trick. Does anyone have input on this one?
This init is called before UIApplication has set up yet, so if you want to register defaults (which can be done from plist of big dictionary) it should be done via app delegate adapter.
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
// Register defaults here !!
UserDefaults.standard.register(defaults: [
"selectedRoundLength": 1
// ... other settings
]
)
return true
}
}
#main
struct TestApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
If your minimum target is iOS 14, you can use #AppStorage to save your value in UserDefaults and start with an initial value. You don't particularly need to add this #AppStorage value in the #main to initialise it. You can just call it in any SwiftUI view or Observable class where you really need it. It will have this initial value if none has been saved.
import SwiftUI
#main
struct TestApp: App {
// This is UserDefaults starting in iOS 14.
#AppStorage("selectedRoundLength") var selectedRoundLength = 1
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
I am new to iOS development and am struggling to implement Firebase phone authentication with the new #App protocol in SwiftUI. The documentation for Firebase is written for UIKit, and the tutorials online for SwiftUI use AppDelegate and SceneDelegate instead of the new #main protocol in the App struct.
My concrete questions are as follows: How/where would I inject this authentication state class I have created?
import Foundation
class AuthenticationState: NSObject, ObservableObject
{
#Published var loggedInUser: User?
#Published var isAuthenticating = false
#Published var error: NSError?
static let shared = AuthenticationState();
private let auth = Auth.auth();
fileprivate var currentNonce: String?
func login(with loginOption: LoginOption) {
self.isAuthenticating = true
self.error = nil
switch loginOption {
case .signInWithApple:
handleSignInWithApple()
}
}
func signup(email: String, password: String, passwordConfirmation: String) {
// TODO
}
private func handleSignInWithApple() {
// TODO
}
}
Secondly, the AuthenticationState class does not know about Firebase Auth object, I assume because it is incorrectly configured. So far, I am configuring Firebase in an AppDelegate class:
import UIKit
import Firebase
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplication.LaunchOptionsKey: Any]?) -> Bool {
FirebaseApp.configure()
return true
}
}
Whereas, I also have a MapworkApp.swift file, which I believe is supposed to replace AppDelegate, but I am unsure:
import SwiftUI
import Firebase
#main
struct MapworkApp: App {
let persistenceController = PersistenceController.shared
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}
struct MapworkApp_Previews: PreviewProvider {
static var previews: some View {
/*#START_MENU_TOKEN#*/Text("Hello, World!")/*#END_MENU_TOKEN#*/
}
}
The runtime errors I currently receive:
020-12-16 13:22:34.416917-0700 Mapwork[1351:332863] 7.3.0 - [Firebase/Core][I-COR000003] The default Firebase app has not yet been configured. Add `[FIRApp configure];` (`FirebaseApp.configure()` in Swift) to your application initialization. Read more:
2020-12-16 13:22:34.417240-0700 Mapwork[1351:332633] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'The default FIRApp instance must be configured before the default FIRAuthinstance can be initialized. One way to ensure that is to call `[FIRApp configure];` (`FirebaseApp.configure()` in Swift) in the App Delegate's `application:didFinishLaunchingWithOptions:` (`application(_:didFinishLaunchingWithOptions:)` in Swift).'
*** First throw call stack:
(0x1863a586c 0x19b314c50 0x18629e4a4 0x102bc8918 0x1028465d8 0x1028466f4 0x102845c1c 0x102845be4 0x102ec96c0 0x102ecb1f8 0x18a32c5bc 0x102845c64 0x102854aa0 0x102854cbc 0x18cdba724 0x102854c18 0x102855028 0x185fda6b0)
libc++abi.dylib: terminating with uncaught exception of type NSException
terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'The default FIRApp instance must be configured before the default FIRAuthinstance can be initialized. One way to ensure that is to call `[FIRApp configure];` (`FirebaseApp.configure()` in Swift) in the App Delegate's `application:didFinishLaunchingWithOptions:` (`application(_:didFinishLaunchingWithOptions:)` in Swift).'
Message from debugger: The LLDB RPC server has exited unexpectedly. Please file a bug if you have reproducible steps.
Any help or direction would be greatly appreciated as the docs online are now inapplicable.
For many Firebase APIs, you don't need to use the AppDelegate approach. Instead, you can get away with initialising Firebase in your app's initialiser, like so:
import SwiftUI
import Firebase
#main
struct MapworkApp: App {
init() {
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
This works well for apps that use Cloud Firestore and Firebase Authentication (Sign in with Apple).
For some of Firebase's SDKs you need to implement an AppDelegate. To do so, implement your AppDelegate like this:
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print("Application is starting up. ApplicationDelegate didFinishLaunchingWithOptions.")
return true
}
// ... add any callbacks your app might require
}
Then, connect your App to the AppDelegate like this:
#main
struct MapworkApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
init() {
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
You might have to move the call to FirebaseApp.configure() to the AppDelegate. In this case, your AppDelegate and App might look like this (you can keep them in the same file, if you like):
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
FirebaseApp.configure()
return true
}
}
#main
struct MapworkApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
To learn more about this, check out my articles about the new application life cycle:
Firebase and the new SwiftUI 2 Application Life Cycle - SwiftUI 2
The Ultimate Guide to the SwiftUI 2 Application Life Cycle - SwiftUI 2
This sample app I wrote shows how to build a simple to-do list app with SwiftUI and Firebase: peterfriese/MakeItSo. The latest version of the code follows the new SwiftUI 2 Application Life Cycle, and includes Sign in with Apple.
The goal is to have easy access to hosting window at any level of SwiftUI view hierarchy. The purpose might be different - close the window, resign first responder, replace root view or contentViewController. Integration with UIKit/AppKit also sometimes require path via window, so…
What I met here and tried before,
something like this
let keyWindow = shared.connectedScenes
.filter({$0.activationState == .foregroundActive})
.map({$0 as? UIWindowScene})
.compactMap({$0})
.first?.windows
.filter({$0.isKeyWindow}).first
or via added in every SwiftUI view UIViewRepresentable/NSViewRepresentable to get the window using view.window looks ugly, heavy, and not usable.
Thus, how would I do that?
SwiftUI Lift-Cycle (SwiftUI 2+)
Here is a solution (tested with Xcode 13.4), to be brief only for iOS
We need application delegate to create scene configuration with our scene delegate class
#main
struct PlayOn_iOSApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
// ...
}
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
if connectingSceneSession.role == .windowApplication {
configuration.delegateClass = SceneDelegate.self
}
return configuration
}
}
Declare our SceneDelegate and confirm it to both (!!!+) UIWindowSceneDelegate and ObservableObject
class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {
var window: UIWindow? // << contract of `UIWindowSceneDelegate`
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
self.window = windowScene.keyWindow // << store !!!
}
}
Now we can use our delegate anywhere (!!!) in view hierarchy as EnvironmentObject, because (bonus of confirming to ObservableObject) SwiftUI automatically injects it into ContentView
#EnvironmentObject var sceneDelegate: SceneDelegate
var body: some View {
// ...
.onAppear {
if let myWindow = sceneDelegate.window {
print(">> window: \(myWindow.description)")
}
}
}
Complete code in project is here
UIKit Life-Cycle
Here is the result of my experiments that looks appropriate for me, so one might find it helpful as well. Tested with Xcode 11.2 / iOS 13.2 / macOS 15.0
The idea is to use native SwiftUI Environment concept, because once injected environment value becomes available for entire view hierarchy automatically. So
Define Environment key. Note, it needs to remember to avoid reference cycling on kept window
struct HostingWindowKey: EnvironmentKey {
#if canImport(UIKit)
typealias WrappedValue = UIWindow
#elseif canImport(AppKit)
typealias WrappedValue = NSWindow
#else
#error("Unsupported platform")
#endif
typealias Value = () -> WrappedValue? // needed for weak link
static let defaultValue: Self.Value = { nil }
}
extension EnvironmentValues {
var hostingWindow: HostingWindowKey.Value {
get {
return self[HostingWindowKey.self]
}
set {
self[HostingWindowKey.self] = newValue
}
}
}
Inject hosting window in root ContentView in place of window creation (either in AppDelegate or in SceneDelegate, just once
// window created here
let contentView = ContentView()
.environment(\.hostingWindow, { [weak window] in
return window })
#if canImport(UIKit)
window.rootViewController = UIHostingController(rootView: contentView)
#elseif canImport(AppKit)
window.contentView = NSHostingView(rootView: contentView)
#else
#error("Unsupported platform")
#endif
use only where needed, just by declaring environment variable
struct ContentView: View {
#Environment(\.hostingWindow) var hostingWindow
var body: some View {
VStack {
Button("Action") {
// self.hostingWindow()?.close() // macOS
// self.hostingWindow()?.makeFirstResponder(nil) // macOS
// self.hostingWindow()?.resignFirstResponder() // iOS
// self.hostingWindow()?.rootViewController?.present(UIKitController(), animating: true)
}
}
}
}
Add the window as a property in an environment object. This can be an existing object that you use for other app-wide data.
final class AppData: ObservableObject {
let window: UIWindow? // Will be nil in SwiftUI previewers
init(window: UIWindow? = nil) {
self.window = window
}
}
Set the property when you create the environment object. Add the object to the view at the base of your view hierarchy, such as the root view.
let window = UIWindow(windowScene: windowScene) // Or however you initially get the window
let rootView = RootView().environmentObject(AppData(window: window))
Finally, use the window in your view.
struct MyView: View {
#EnvironmentObject private var appData: AppData
// Use appData.window in your view's body.
}
Access the current window by receiving NSWindow.didBecomeKeyNotification:
.onReceive(NotificationCenter.default.publisher(for: NSWindow.didBecomeKeyNotification)) { notification in
if let window = notification.object as? NSWindow {
// ...
}
}
At first I liked the answer given by #Asperi, but when trying it in my own environment I found it difficult to get working due to my need to know the root view at the time I create the window (hence I don't know the window at the time I create the root view). So I followed his example, but instead of an environment value I chose to use an environment object. This has much the same effect, but was easier for me to get working. The following is the code that I use. Note that I have created a generic class that creates an NSWindowController given a SwiftUI view. (Note that the userDefaultsManager is another object that I need in most of the windows in my application. But I think if you remove that line plus the appDelegate line you would end up with a solution that would work pretty much anywhere.)
class RootViewWindowController<RootView : View>: NSWindowController {
convenience init(_ title: String,
withView rootView: RootView,
andInitialSize initialSize: NSSize = NSSize(width: 400, height: 500))
{
let appDelegate: AppDelegate = NSApplication.shared.delegate as! AppDelegate
let windowWrapper = NSWindowWrapper()
let actualRootView = rootView
.frame(width: initialSize.width, height: initialSize.height)
.environmentObject(appDelegate.userDefaultsManager)
.environmentObject(windowWrapper)
let hostingController = NSHostingController(rootView: actualRootView)
let window = NSWindow(contentViewController: hostingController)
window.setContentSize(initialSize)
window.title = title
windowWrapper.rootWindow = window
self.init(window: window)
}
}
final class NSWindowWrapper: ObservableObject {
#Published var rootWindow: NSWindow? = nil
}
Then in my view where I need it (in order to close the window at the appropriate time), my struct begins as the following:
struct SubscribeToProFeaturesView: View {
#State var showingEnlargedImage = false
#EnvironmentObject var rootWindowWrapper: NSWindowWrapper
var body: some View {
VStack {
Text("Professional Version Upgrade")
.font(.headline)
VStack(alignment: .leading) {
And in the button where I need to close the window I have
self.rootWindowWrapper.rootWindow?.close()
It's not quite as clean as I would like it to be (I would prefer to have a solution where I did just say self.rootWindow?.close() instead of requiring the wrapper class), but it isn't bad and it allows me to create the rootView object before I create the window.
Instead of ProjectName_App use old fashioned AppDelegate approach as app entry point.
#main
final class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
...
}
}
Then pass window as environment object. For example:
struct WindowKey: EnvironmentKey {
static let defaultValue: UIWindow? = nil
}
extension EnvironmentValues {
var window: WindowKey.Value {
get { return self[WindowKey.self] }
set { self[WindowKey.self] = newValue }
}
}
final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(windowScene: windowScene)
let rootView = RootView()
.environment(\.window, window)
window?.rootViewController = UIHostingController(rootView: rootView)
window?.makeKeyAndVisible()
}
}
And use it when need it.
struct ListCell: View {
#Environment(\.window) private var window
var body: some View {
Rectangle()
.onTapGesture(perform: share)
}
private func share() {
let vc = UIActivityViewController(activityItems: [], applicationActivities: nil)
window?.rootViewController?.present(vc, animated: true)
}
}
2022, macOS only
Maybe not best solution, but works well for me and enough universal for almost any situation
Usage:
someView()
.wndAccessor {
$0?.title = String(localized: "All you need to know about FileBo in ONE minute")
}
extension code:
import SwiftUI
#available(OSX 11.0, *)
public extension View {
func wndAccessor(_ act: #escaping (NSWindow?) -> () )
-> some View {
self.modifier(WndTitleConfigurer(act: act))
}
}
#available(OSX 11.0, *)
struct WndTitleConfigurer: ViewModifier {
let act: (NSWindow?) -> ()
#State var window: NSWindow? = nil
func body(content: Content) -> some View {
content
.getWindow($window)
.onChange(of: window, perform: act )
}
}
//////////////////////////////
///HELPERS
/////////////////////////////
// Don't use this:
// Usage:
//.getWindow($window)
//.onChange(of: window) { _ in
// if let wnd = window {
// wnd.level = .floating
// }
//}
#available(OSX 11.0, *)
private extension View {
func getWindow(_ wnd: Binding<NSWindow?>) -> some View {
self.background(WindowAccessor(window: wnd))
}
}
#available(OSX 11.0, *)
private struct WindowAccessor: NSViewRepresentable {
#Binding var window: NSWindow?
public func makeNSView(context: Context) -> NSView {
let view = NSView()
DispatchQueue.main.async {
self.window = view.window
}
return view
}
public func updateNSView(_ nsView: NSView, context: Context) {}
}