What is equivalent to 'window.rootViewController' in WindowGroup - SwiftUI - swift

I am new to SwiftUI and facing a problem where I want to change the root view when a certain action occurs inside the app.
How I handle it when using SceneDelegate was as follows
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
... // the code of initializing the window
observeOnChangeWindow()
}
func observeOnChangeWindow(){
NotificationCenter.default.addObserver(self, selector: #selector(self.performChangeWindow), name: Notification.Name(K.changeWindowNotificationName), object: nil)
}
#objc func performChangeWindow() {
self.window?.rootViewController = UIHostingController(rootView: SplashScreenView())
}
However, I am not currently using SceneDelegate as I am initializing the app using WindowGroup
struct MyApp: App {
var body: some Scene {
WindowGroup {
SplashScreenView()
}
}
}
My question is :
How can I perform the same thing I am doing using SceneDelegate now ?

With the help of comments and some tutorials, I reached out to the solution (Tested on iOS 15, Xcode 13.2.1):
Add the following code to the Main App Launcher.
struct MyApp: App {
#StateObject var appState = AppState.shared
var body: some Scene {
WindowGroup {
SplashScreenView().id(appState.environment)
}
}
}
And then I created the AppState class, which is the class that when changes I will change the window.
class AppState: ObservableObject {
static let shared = AppState()
#Published var environment = "Production"
}
And whenever I wanted to change the environment and do the same functionality of changing window in UIKit , do the following :
AppState.shared.environment = "Pre-Production"

Related

Using UIApplicationDelegateAdaptor to get callbacks from userDidAcceptCloudKitShareWith not working

I'm trying to get notified when userDidAcceptCloudKitShareWith gets called. Traditionally this was called in the App Delegate but since I am building an iOS 14+ using App as my root object. I couldn't find any documentation out yet as far as how to add userDidAcceptCloudKitShareWith to my App class, so I am using UIApplicationDelegateAdaptor to use an App Delegate class, however it doesn't seem like userDidAcceptCloudKitShareWith is ever getting called?
import SwiftUI
import CloudKit
// Our observable object class
class ShareDataStore: ObservableObject {
static let shared = ShareDataStore()
#Published var didRecieveShare = false
#Published var shareInfo = ""
}
#main
struct SocialTestAppApp: App {
#StateObject var shareDataStore = ShareDataStore.shared
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView().environmentObject(shareDataStore)
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate {
let container = CKContainer(identifier: "iCloud.com.TestApp")
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print("did finish launching called")
return true
}
func application(_ application: UIApplication, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) {
print("delegate callback called!! ")
acceptShare(metadata: cloudKitShareMetadata) { result in
switch result {
case .success(let recordID):
print("successful share!")
ShareDataStore.shared.didRecieveShare = true
ShareDataStore.shared.shareInfo = recordID.recordName
case .failure(let error):
print("failure in share = \(error)")
}
} }
func acceptShare(metadata: CKShare.Metadata,
completion: #escaping (Result<CKRecord.ID, Error>) -> Void) {
// Create a reference to the share's container so the operation
// executes in the correct context.
let container = CKContainer(identifier: metadata.containerIdentifier)
// Create the operation using the metadata the caller provides.
let operation = CKAcceptSharesOperation(shareMetadatas: [metadata])
var rootRecordID: CKRecord.ID!
// If CloudKit accepts the share, cache the root record's ID.
// The completion closure handles any errors.
operation.perShareCompletionBlock = { metadata, share, error in
if let _ = share, error == nil {
rootRecordID = metadata.rootRecordID
}
}
// If the operation fails, return the error to the caller.
// Otherwise, return the record ID of the share's root record.
operation.acceptSharesCompletionBlock = { error in
if let error = error {
completion(.failure(error))
} else {
completion(.success(rootRecordID))
}
}
// Set an appropriate QoS and add the operation to the
// container's queue to execute it.
operation.qualityOfService = .utility
container.add(operation)
}
}
Updated based on Asperi's Answer:
import SwiftUI
import CloudKit
class ShareDataStore: ObservableObject {
static let shared = ShareDataStore()
#Published var didRecieveShare = false
#Published var shareInfo = ""
}
#main
struct athlyticSocialTestAppApp: App {
#StateObject var shareDataStore = ShareDataStore.shared
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
let sceneDelegate = MySceneDelegate()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(shareDataStore)
.withHostingWindow { window in
sceneDelegate.originalDelegate = window.windowScene.delegate
window.windowScene.delegate = sceneDelegate
}
}
}
}
class MySceneDelegate: NSObject, UIWindowSceneDelegate {
let container = CKContainer(identifier: "iCloud.com...")
var originalDelegate: UIWindowSceneDelegate?
var window: UIWindow?
func sceneWillEnterForeground(_ scene: UIScene) {
print("scene is active")
}
func sceneWillResignActive(_ scene: UIScene) {
print("scene will resign active")
}
// forward all other UIWindowSceneDelegate/UISceneDelegate callbacks to original, like
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
originalDelegate?.scene!(scene, willConnectTo: session, options: connectionOptions)
}
func windowScene(_ windowScene: UIWindowScene, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) {
print("delegate callback called!! ")
acceptShare(metadata: cloudKitShareMetadata) { result in
switch result {
case .success(let recordID):
print("successful share!")
ShareDataStore.shared.didRecieveShare = true
ShareDataStore.shared.shareInfo = recordID.recordName
case .failure(let error):
print("failure in share = \(error)")
}
}
}
}
extension View {
func withHostingWindow(_ callback: #escaping (UIWindow?) -> Void) -> some View {
self.background(HostingWindowFinder(callback: callback))
}
}
struct HostingWindowFinder: UIViewRepresentable {
var callback: (UIWindow?) -> ()
func makeUIView(context: Context) -> UIView {
let view = UIView()
DispatchQueue.main.async { [weak view] in
self.callback(view?.window)
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
}
}
In Scene-based application the userDidAcceptCloudKitShareWith callback is posted to Scene delegate, but in SwiftUI 2.0 App-based application the scene delegate is used by SwiftUI itself to provide scenePhase events, but does not provide native way to handle topic callback.
The possible approach to solve this is to find a window and inject own scene delegate wrapper, which will handle userDidAcceptCloudKitShareWith and forward others to original SwiftUI delegate (to keep standard SwiftUI events working).
Here is a couple of demo snapshots based on https://stackoverflow.com/a/63276688/12299030 window access (however you can use any other preferable way to get window)
#main
struct athlyticSocialTestAppApp: App {
#StateObject var shareDataStore = ShareDataStore.shared
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
let sceneDelegate = MySceneDelegate()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(shareDataStore)
.withHostingWindow { window in
sceneDelegate.originalDelegate = window?.windowScene.delegate
window?.windowScene.delegate = sceneDelegate
}
}
}
}
class MySceneDelegate : NSObject, UIWindowSceneDelegate {
var originalDelegate: UISceneDelegate?
func windowScene(_ windowScene: UIWindowScene, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShareMetadata) {
// your code here
}
// forward all other UIWindowSceneDelegate/UISceneDelegate callbacks to original, like
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
originalDelegate?.scene(scene, willConnectTo: session, options: connectionOptions)
}
}
Check out this question that has a lot of useful things to check across several possible answers:
CloudKit CKShare userDidAcceptCloudKitShareWith Never Fires on Mac App
Be sure to add the CKSharingSupported key to your info.plist, and then try putting the userDidAcceptCloudKitShareWith in different places using the answers in the above link (where you put it will depend on what kind of app you're building).

Converting SceneDelegate Enviromental Object to AppMain

Feel free to edit my title for better clarity.
I am starting a new iOS project and am no longer using SceneDelegate/AppDelegate. My problem is I want my ObservableObject to be an Environmental Object for my entire project but am having trouble converting and finding recent examples.
This is how I defined it in my previous iOS 13 project.
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions){
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
//Environmental View
let observer = GlobalObserver()
let baseView = SplashScreenView().environment(\.managedObjectContext, context)
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: baseView.environmentObject(observer))
self.window = window
window.makeKeyAndVisible()
}
}
Here is my main simplified
#main
struct DefaultApp: App {
//Environmental View
let observer: GlobalObserver
init(){
observer = GlobalObserver()
}
var body: some Scene {
WindowGroup {
LoginView()
//.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}
The project generated a PersistenceController which I presume had to do with local storage. Do I need to somehow pass my observer into the .environment for loginView?
Yes, it's exactly as you have it in your example code, except that for some reason you commented it all out. So for instance:
class Thing : ObservableObject {
#Published var name = "Matt"
}
#main
struct SwiftUIApp: App {
#StateObject var thing = Thing()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(thing)
}
}
}
Now in any View struct you just say
#EnvironmentObject var thing : Thing
and presto, you're observing from the global instance.

Where to place app delegate code in App.swift file?

I'm trying to get Snapkit working with SwiftUI to allow logins via SnapChat. I'm following along with this StackOverflow question (Can I use the Snapchat SDK (SnapKit) with SwiftUI?) but I'm having trouble getting the accepted solution to work. The code posted as the answer was intended to go in the app delegate file but as of the latest version of XCode they are no longer used. Instead, the code snippet needs to be placed in the AppName.swift file but my breakpoint doesn't trigger. Here's my current version of my App.swift file:
import SwiftUI
import SCSDKLoginKit
#main
struct SampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
for urlContext in URLContexts {
let url = urlContext.url
var options: [UIApplication.OpenURLOptionsKey : Any] = [:]
options[.openInPlace] = urlContext.options.openInPlace
options[.sourceApplication] = urlContext.options.sourceApplication
options[.annotation] = urlContext.options.annotation
SCSDKLoginClient.application(UIApplication.shared, open: url, options: options)
}
}
Any help is greatly appreciated. Thanks!
EDIT: Here's the solution that worked thanks to Asperi! Updated code here in case anyone runs into this:
import SwiftUI
import SCSDKLoginKit
#main
struct SampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in
SCSDKLoginClient.application(UIApplication.shared, open: url)
}
}
}
}
You should use .onOpenURL instead, like
#main
struct SampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in
// .. do whatever needed here
}
}
}
}

Use ViewRouter in SwiftUI Project in Xcode 12

I tried to use this method (see link attached) in a new Xcode 12 project as a way to create a login page for a SwiftUI app, but I had the Problem not knowing what to add to the main App struct. I'm still a beginner and tried adding ContentView().environmentObject(ViewRouter()) to the WindowGroup in the main app struct. Am I totally wrong or why doesn't Xcode build the view? Can somebody help?
Below the working code snippet:
import SwiftUI
import Foundation
import Combine
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: MotherView().environmentObject(ViewRouter()))
self.window = window
window.makeKeyAndVisible()
}
}
. . .
}
class ViewRouter: ObservableObject {
let objectWillChange = PassthroughSubject<ViewRouter,Never>()
var currentPage: String = "page1" {
didSet {
withAnimation() {
objectWillChange.send(self)
}
}
}
}
struct MotherView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
if viewRouter.currentPage == "page1" {
ContentViewA()
} else if viewRouter.currentPage == "page2" {
ContentViewB()
.transition(.scale)
}
}
}
}
struct ContentViewA : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
Button(action: {self.viewRouter.currentPage = "page2"}) {
Text("Login")
}
}
}
struct ContentViewB : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
Button(action: {self.viewRouter.currentPage = "page1"}) {
Text("Logout")
}
}
}
Now I want to substitute the SceneDelegate in the Xcode 12 style, but the following doesn't work. Any idea why?
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
MotherView().environmentObject(ViewRouter())
}
}
}
Pls try next steps:
remove whole SceneDelegate class (in your case no need SceneDelegate class)
Modify your ViewRouter class like:
class ViewRouter: ObservableObject {
#Published var currentPage: String = "page1"
}

Using #EnvironmentObject properties with CADisplayLink

I'm trying to implement CADisplayLink for some animations, but when I try to access my MainData environment object properties from inside class MyAnimations, I get the fatal error No ObservableObject of type MainData found. A View.environmentObject(_:) for MainData may be missing as an ancestor of this view.
In SceneDelegate, I have MainData set as an environment object on ContentView:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var mainData = MainData()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(self.mainData))
self.window = window
window.makeKeyAndVisible()
}
}
...
}
And here's the class with CADisplayLink. createDisplayLink() is called from ContentView:
class MyAnimations: NSObject{
#EnvironmentObject var mainData: MainData
func createDisplayLink() {
let displaylink = CADisplayLink(target: self, selector: #selector(step))
displaylink.add(to: .current, forMode: RunLoop.Mode.default)
}
#objc func step(link: CADisplayLink) {
mainData.displayLinkY += 1.5 //Error here
mainData.displayLinkX += 1.5
}
}
My question is: how can I change environment object properties displayLinkX and displayLinkY from inside step()?
Just remove #EnvironmentObject property wrapper, it is for SwiftUI only
class MyAnimations: NSObject{
var mainData: MainData
init(mainData: MainData) {
self.mainData = mainData
super.init()
}
// ... other code
}