Use ViewRouter in SwiftUI Project in Xcode 12 - swift

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"
}

Related

How to implement multi window with menu commands in SwiftUI for macOS?

Situation
Implement a multi window application, where each window has its own state.
Example
Here is an example (on github) to showcase the question:
import SwiftUI
#main
struct multi_window_menuApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}.commands {
MenuCommands()
}
}
}
struct ContentView: View {
#StateObject var viewModel: ViewModel = ViewModel()
var body: some View {
TextField("", text: $viewModel.inputText)
.disabled(true)
.padding()
}
}
public class ViewModel: ObservableObject {
#Published var inputText: String = "" {
didSet {
print("content was updated...")
}
}
}
Question
How should we programmatically figure out what is the currently selected view so we can update the state when the menu command is about to finish and update the state in the view model?
import Foundation
import SwiftUI
import Combine
struct MenuCommands: Commands {
var body: some Commands {
CommandGroup(after: CommandGroupPlacement.newItem, addition: {
Divider()
Button(action: {
let dialog = NSOpenPanel();
dialog.title = "Choose a file";
dialog.showsResizeIndicator = true;
dialog.showsHiddenFiles = false;
dialog.allowsMultipleSelection = false;
dialog.canChooseDirectories = false;
if (dialog.runModal() == NSApplication.ModalResponse.OK) {
let result = dialog.url
if (result != nil) {
let path: String = result!.path
do {
let string = try String(contentsOf: URL(fileURLWithPath: path), encoding: .utf8)
print(string)
// how to get access to the currently active view model to update the inputText variable?
// viewModel.inputText = string
}
catch {
print("Error \(error)")
}
}
} else {
return
}
}, label: {
Text("Open File")
})
.keyboardShortcut("O", modifiers: .command)
})
}
}
Links that might be useful to figure this out:
http://www.gfrigerio.com/build-a-macos-app-with-swiftui/
https://troz.net/post/2021/swiftui_mac_menus/
https://onmyway133.com/posts/how-to-manage-windowgroup-in-swiftui-for-macos/
Useful links:
How to access NSWindow from #main App using only SwiftUI?
How to access own window within SwiftUI view?
https://lostmoa.com/blog/ReadingTheCurrentWindowInANewSwiftUILifecycleApp/
(this is what I was able to come up with, if anyone has a better idea/approach, please share)
The idea is to create a shared "global" view model that keeps track of opened windows and view models. Each NSWindow has an attribute with a unique windowNumber. When a window becomes active (key), it looks up the view model by the windowNumber and sets it as the activeViewModel.
import SwiftUI
class GlobalViewModel : NSObject, ObservableObject {
// all currently opened windows
#Published var windows = Set<NSWindow>()
// all view models that belong to currently opened windows
#Published var viewModels : [Int:ViewModel] = [:]
// currently active aka selected aka key window
#Published var activeWindow: NSWindow?
// currently active view model for the active window
#Published var activeViewModel: ViewModel?
func addWindow(window: NSWindow) {
window.delegate = self
windows.insert(window)
}
// associates a window number with a view model
func addViewModel(_ viewModel: ViewModel, forWindowNumber windowNumber: Int) {
viewModels[windowNumber] = viewModel
}
}
Then, react on every change on window (when it is being closed and when it becomes an active aka key window):
import SwiftUI
extension GlobalViewModel : NSWindowDelegate {
func windowWillClose(_ notification: Notification) {
if let window = notification.object as? NSWindow {
windows.remove(window)
viewModels.removeValue(forKey: window.windowNumber)
print("Open Windows", windows)
print("Open Models", viewModels)
}
}
func windowDidBecomeKey(_ notification: Notification) {
if let window = notification.object as? NSWindow {
print("Activating Window", window.windowNumber)
activeWindow = window
activeViewModel = viewModels[window.windowNumber]
}
}
}
Provide a way to lookup window that is associated to the current view:
import SwiftUI
struct HostingWindowFinder: NSViewRepresentable {
var callback: (NSWindow?) -> ()
func makeNSView(context: Self.Context) -> NSView {
let view = NSView()
DispatchQueue.main.async { [weak view] in
self.callback(view?.window)
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {}
}
Here is the view that is updating the global view model with the current window and viewModel:
import SwiftUI
struct ContentView: View {
#EnvironmentObject var globalViewModel : GlobalViewModel
#StateObject var viewModel: ViewModel = ViewModel()
var body: some View {
HostingWindowFinder { window in
if let window = window {
self.globalViewModel.addWindow(window: window)
print("New Window", window.windowNumber)
self.globalViewModel.addViewModel(self.viewModel, forWindowNumber: window.windowNumber)
}
}
TextField("", text: $viewModel.inputText)
.disabled(true)
.padding()
}
}
Then we need to create the global view model and send it to the views and commands:
import SwiftUI
#main
struct multi_window_menuApp: App {
#State var globalViewModel = GlobalViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(self.globalViewModel)
}
.commands {
MenuCommands(globalViewModel: self.globalViewModel)
}
Settings {
VStack {
Text("My Settingsview")
}
}
}
}
Here is how the commands look like, so they can access the currently selected/active viewModel:
import Foundation
import SwiftUI
import Combine
struct MenuCommands: Commands {
var globalViewModel: GlobalViewModel
var body: some Commands {
CommandGroup(after: CommandGroupPlacement.newItem, addition: {
Divider()
Button(action: {
let dialog = NSOpenPanel();
dialog.title = "Choose a file";
dialog.showsResizeIndicator = true;
dialog.showsHiddenFiles = false;
dialog.allowsMultipleSelection = false;
dialog.canChooseDirectories = false;
if (dialog.runModal() == NSApplication.ModalResponse.OK) {
let result = dialog.url
if (result != nil) {
let path: String = result!.path
do {
let string = try String(contentsOf: URL(fileURLWithPath: path), encoding: .utf8)
print("Active Window", self.globalViewModel.activeWindow?.windowNumber)
self.globalViewModel.activeViewModel?.inputText = string
}
catch {
print("Error \(error)")
}
}
} else {
return
}
}, label: {
Text("Open File")
})
.keyboardShortcut("O", modifiers: [.command])
})
}
}
All is updated and runnable under this github project: https://github.com/ondrej-kvasnovsky/swiftui-multi-window-menu
I came across this question when I was solving a similar problem. I believe the SwiftUI way is to use FocusedValue:
// create an active viewmodel key
struct ActiveViewModelKey: FocusedValueKey {
typealias Value = ViewModel
}
extension FocusedValues {
var activeViewModel: ViewModel? {
get { self[ActiveViewModelKey.self] }
set { self[ActiveViewModelKey.self] = newValue }
}
}
struct ContentView: View {
#StateObject var viewModel: ViewModel = ViewModel()
var body: some View {
TextField("", text: $viewModel.inputText)
...
.focusedValue(\.activeViewModel, viewModel) // inject the focused value
}
}
struct MenuCommands: Commands {
#FocusedValue(\.activeViewModel) var activeViewModel // inject the active viewmodel
var body: some Commands {
CommandGroup(after: CommandGroupPlacement.newItem, addition: {
Divider()
Button(action: {
...
activeViewModel?.inputText = string
}, label: {
Text("Open File")
})
.keyboardShortcut("O", modifiers: [.command])
})
}
}

SwiftUI generic navigation link using block variable

Here I would like to create navigation code, I'm able to do this Swift UIKit. I'm trying the same functionality in SwiftUI, but I'm facing an issue with my code. How can we convert the SwiftUI view to AnyView.
Is there any other way to achieve the same functionality in SwiftUI?
Your help would be greatly appreciated.!!
/// Swift code
public struct Navigator {
public var onLoginSuccess: (UINavigationController) -> Void = { navigationController in
navigationController.pushViewController(UIViewController(), animated: true)
}
}
/// Usage
var router = Navigator()
router.onLoginSuccess = { nav in
nav.pushViewController(UIViewController(), animated: true)
}
/// SwiftUI Code
struct Navigator {
static var onTap: (AnyView) -> Void = { view in
_ = view.navigate(to: Text("SS"))
}
}
extension View {
func navigate<SomeView: View>(to view: SomeView) -> some View {
modifier(NavigateModifier(destination: view))
}
}
fileprivate struct NavigateModifier<SomeView: View>: ViewModifier {
fileprivate let destination: SomeView
fileprivate func body(content: Content) -> some View {
NavigationView {
ZStack {
content
NavigationLink(destination: destination) {
EmptyView()
}
}
}
}
}
/// Usage
NavigationView {
Button("Home") {
Navigator.onTap(self)
}
}
Here is another solution code working fine with a single destination, but I can't change destination runtime. Router.onLogin should accept destination view.
struct ContentView: View {
var body: some View {
NavigationView {
HStack {
NavigationLink(destination: Router.onLogin) {
Text("HOME")
}
}
}
}
}
struct Router {
#ViewBuilder
static var onLogin: some View {
Text("Hello")
}
}
This wouldn't work because in Navigator onTap return void and it will not push view on any view.
But you can do by this
extension View {
/// Navigate to a new view.
/// - Parameters:
/// - view: View to navigate to.
/// - binding: Active binding
func navigate<NewView: View>(to view: NewView, when binding: Binding<Bool>) -> some View {
ZStack {
self
NavigationLink(
destination: view,
isActive: binding
) {
EmptyView()
}
}
}
}
Usage:
struct ContentView: View {
#State private var isNextScreen: Bool = false
var body: some View {
NavigationView {
Button("Home") {
isNextScreen.toggle()
}.navigate(to: Text("SS"),when: $isNextScreen)
}
}
}
Update
As you mention in a comment, you want multiple and dynamic destinations.
Then you can use it this way.
View extension for navigation
extension View {
func navigate(to view: Binding<Navigator?>) -> some View {
ZStack {
self
if let wrappedValue = view.wrappedValue {
NavigationLink(
destination: wrappedValue.navigateView,
tag: wrappedValue,
selection: view,
label: {EmptyView()})
}
}
}
}
Create Navigator
enum Navigator: Identifiable {
case onTap
case onLogin
var id: Navigator {
return self
}
#ViewBuilder
var navigateView: some View {
switch self {
case .onTap:
Text("SS")
case .onLogin:
Text("Login View")
}
}
}
Usage Content View
struct ContentView: View {
#State private var nextScreen: Navigator? = nil
var body: some View {
NavigationView {
VStack{
Button("Home") {
nextScreen = .onTap
}
Button("Login") {
nextScreen = .onLogin
}
}.navigate(to: $nextScreen)
}
}
}
Here I got one solution to this issue, but not sure it's the best. I believe still we can improve this.
struct LoginView: View {
var loginAction = Router().onLogin(AnyView(Text("Actual View Wll This")))
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: loginAction) {
Text("Show View")
}
NavigationLink(destination: AnyView(Text("Detail"))) {
Text("Show Detail View")
}
}
}
}
}
extension LoginView {
struct Router {
var onLogin:(AnyView) -> AnyView = { links in
return AnyView(links)
}
}
}
/// Now I can change the 'Destination' for 'loginAction' in a scene and where ever I want.
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
var contentView = LoginView()
contentView.loginAction = AnyView(Text("Changed Route to here"))
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
// After some improvements
struct LoginView: View {
var loginAction = Router().onLogin(Text("Actual Detail View"))
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: loginAction) {
Text("Show View")
}
}
}
}
}
extension LoginView {
struct Router<Destination: View> {
var onLogin:(Destination) -> Destination = { links in
return links
}
}
}
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
var contentView = LoginView()
contentView.loginAction = LoginView.Router().onLogin(Text("Changed Route here .!!"))
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}

SwiftUI: Value of optional type 'Binding<String>?' must be unwrapped to a value of type 'Binding<String>'

I am currently learning SwiftUI and trying to understand #EnvironmentObject, which I find a bit confusing.
I want to share a store containing some data throughout my app, so I wrote this:
class Store: ObservableObject {
#Published var user: User?
}
class User: ObservableObject {
#Published var name: String?
}
My app view:
struct AppView: View {
#EnvironmentObject var store: Store
var body: some View {
ContentView()
.environmentObject(store)
}
}
}
SceneDelegate:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var store = Store()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
let appView = AppView().environmentObject(store)
window.rootViewController = UIHostingController(rootView: appView)
self.window = window
window.makeKeyAndVisible()
}
}
}
And in ContentView:
struct ContentView: View {
#EnvironmentObject var store: Store
var body: some View {
VStack {
TextField("Type some text here", text: $store.user?.name ?? "")
}
}
}
However, I am getting an error on the line where I declare the TextField:
Value of optional type 'Binding?' must be unwrapped to a value of type 'Binding'
I don't understand what this means, and don't know what to do here.
Thank you for your help
Here is a possible solution for the case if both user and name variables can be nil - you need to use a custom binding:
struct ContentView: View {
#EnvironmentObject var store: Store
var binding: Binding<String> {
.init(get: {
store.user?.name ?? ""
}, set: {
store.user?.name = $0
})
}
var body: some View {
VStack {
TextField("Type some text here", text: binding)
}
}
}
But you should rather consider displaying this TextField only when the user variable is present. Otherwise what are you really editing? It can't be the name of the user if the user is missing...
Alternatively, try making both these variables non-optional (and preferably changing User to struct):
class Store: ObservableObject {
#Published var user = User(name: "test") // init here or in the `init`
}
struct User {
var name: String // assuming that a `User` always has a `name`
}
and the TextField will work just fine:
TextField("Type some text here", text: $store.user.name)

Reinstantiate an #EnvironmentObject in a view

In my application, I have the need to reinstantiate an #EnvironmentObject at the tap of a button. Where is the right spot to do that?
Here's a screenshot of my view - in the Archived Objects list, I want to have immutable objects. So, when I tap Archive Object, I want to add the current object to the array and create a new current object.
A simplified version of the app looks as follows.
I instantiate the object in the SceneDelegate:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var myObject = MyObject()
let myObjects = MyObjects()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView()
.environmentObject(myObject)
.environmentObject(myObjects)
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
//.....
}
Here's my models:
class MyObject: ObservableObject, Identifiable {
#Published var id = UUID()
}
class MyObjects: ObservableObject {
#Published var values: [MyObject] = [
MyObject(),
MyObject()
]
}
In my view, I have the need to put my current object to an (archive) array and then create a new current object to work with. Here's what I'm trying:
struct ContentView: View {
#EnvironmentObject var currentObject: MyObject
#EnvironmentObject var objects: MyObjects
var sceneDelegate: UISceneDelegate {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let sd = windowScene.delegate as? SceneDelegate else { fatalError() }
return sd
}
var body: some View {
VStack {
Form {
Section(header: Text("Current Object")) {
Text(currentObject.id.uuidString)
}
Section(header: Text("Archived Objects")) {
List(objects.values, id: \.id) { object in
Text(object.id.uuidString)
}
Button(action: {
self.objects.values.append(self.currentObject)
// as the environment object is get only, I cannot reinstantiate it here...
// self.currentObject = MyObject()
}) {
Text("Archive Object")
}
}
}
Spacer()
}
}
}
Here is a solution. See comments inline.
Tested with Xcode 11.4 / iOS 13.4
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
// Usage
ContentView(current: MyObject())
.environmentObject(MyObjects())
}
}
struct ContentView: View {
#EnvironmentObject var objects: MyObjects
#State private var current: MyObject // tracks current
init(current: MyObject) {
_current = State(initialValue: current)
}
var body: some View {
// rebuilt on *current* changed
ContentInnerView(current: $current)
.environmentObject(current)
}
struct ContentInnerView: View {
#Binding var current: MyObject // change current
#EnvironmentObject var objects: MyObjects
#EnvironmentObject var currentObject: MyObject
init(current: Binding<MyObject>) {
_current = current
}
var sceneDelegate: UISceneDelegate {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let sd = windowScene.delegate as? SceneDelegate else { fatalError() }
return sd
}
var body: some View {
VStack {
Form {
Section(header: Text("Current Object")) {
Text(currentObject.id.uuidString)
}
Section(header: Text("Archived Objects")) {
List(objects.values, id: \.id) { object in
Text(object.id.uuidString)
}
Button(action: {
self.objects.values.append(self.currentObject)
self.current = MyObject() // << works !!
}) {
Text("Archive Object")
}
}
}
Spacer()
}
}
}
}

Programmatically navigate to new view in SwiftUI

Descriptive example:
login screen, user taps "Login" button, request is performed, UI shows waiting indicator, then after successful response I'd like to automatically navigate user to the next screen.
How can I achieve such automatic transition in SwiftUI?
You can replace the next view with your login view after a successful login. For example:
struct LoginView: View {
var body: some View {
...
}
}
struct NextView: View {
var body: some View {
...
}
}
// Your starting view
struct ContentView: View {
#EnvironmentObject var userAuth: UserAuth
var body: some View {
if !userAuth.isLoggedin {
LoginView()
} else {
NextView()
}
}
}
You should handle your login process in your data model and use bindings such as #EnvironmentObject to pass isLoggedin to your view.
Note: In Xcode Version 11.0 beta 4, to conform to protocol 'BindableObject' the willChange property has to be added
import Combine
class UserAuth: ObservableObject {
let didChange = PassthroughSubject<UserAuth,Never>()
// required to conform to protocol 'ObservableObject'
let willChange = PassthroughSubject<UserAuth,Never>()
func login() {
// login request... on success:
self.isLoggedin = true
}
var isLoggedin = false {
didSet {
didChange.send(self)
}
// willSet {
// willChange.send(self)
// }
}
}
For future reference, as a number of users have reported getting the error "Function declares an opaque return type", to implement the above code from #MoRezaFarahani requires the following syntax:
struct ContentView: View {
#EnvironmentObject var userAuth: UserAuth
var body: some View {
if !userAuth.isLoggedin {
return AnyView(LoginView())
} else {
return AnyView(NextView())
}
}
}
This is working with Xcode 11.4 and Swift 5
struct LoginView: View {
#State var isActive = false
#State var attemptingLogin = false
var body: some View {
ZStack {
NavigationLink(destination: HomePage(), isActive: $isActive) {
Button(action: {
attlempinglogin = true
// Your login function will most likely have a closure in
// which you change the state of isActive to true in order
// to trigger a transition
loginFunction() { response in
if response == .success {
self.isActive = true
} else {
self.attemptingLogin = false
}
}
}) {
Text("login")
}
}
WaitingIndicator()
.opacity(attemptingLogin ? 1.0 : 0.0)
}
}
}
Use Navigation link with the $isActive binding variable
To expound what others have elaborated above based on changes on combine as of Swift Version 5.2 it could be simplified using publishers.
Create a class names UserAuth as shown below don't forget to import import Combine.
class UserAuth: ObservableObject {
#Published var isLoggedin:Bool = false
func login() {
self.isLoggedin = true
}
}
Update SceneDelegate.Swift with
let contentView = ContentView().environmentObject(UserAuth())
Your authentication view
struct LoginView: View {
#EnvironmentObject var userAuth: UserAuth
var body: some View {
...
if ... {
self.userAuth.login()
} else {
...
}
}
}
Your dashboard after successful authentication, if the authentication userAuth.isLoggedin = true then it will be loaded.
struct NextView: View {
var body: some View {
...
}
}
Lastly, the initial view to be loaded once the application is launched.
struct ContentView: View {
#EnvironmentObject var userAuth: UserAuth
var body: some View {
if !userAuth.isLoggedin {
LoginView()
} else {
NextView()
}
}
}
Here is an extension on UINavigationController that has simple push/pop with SwiftUI views that gets the right animations. The problem I had with most custom navigations above was that the push/pop animations were off. Using NavigationLink with an isActive binding is the correct way of doing it, but it's not flexible or scalable. So below extension did the trick for me:
/**
* Since SwiftUI doesn't have a scalable programmatic navigation, this could be used as
* replacement. It just adds push/pop methods that host SwiftUI views in UIHostingController.
*/
extension UINavigationController: UINavigationControllerDelegate {
convenience init(rootView: AnyView) {
let hostingView = UIHostingController(rootView: rootView)
self.init(rootViewController: hostingView)
// Doing this to hide the nav bar since I am expecting SwiftUI
// views to be wrapped in NavigationViews in case they need nav.
self.delegate = self
}
public func pushView(view:AnyView) {
let hostingView = UIHostingController(rootView: view)
self.pushViewController(hostingView, animated: true)
}
public func popView() {
self.popViewController(animated: true)
}
public func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
navigationController.navigationBar.isHidden = true
}
}
Here is one quick example using this for the window.rootViewController.
var appNavigationController = UINavigationController.init(rootView: rootView)
window.rootViewController = appNavigationController
window.makeKeyAndVisible()
// Now you can use appNavigationController like any UINavigationController, but with SwiftUI views i.e.
appNavigationController.pushView(view: AnyView(MySwiftUILoginView()))
I followed Gene's answer but there are two issues with it that I fixed below. The first is that the variable isLoggedIn must have the property #Published in order to work as intended. The second is how to actually use environmental objects.
For the first, update UserAuth.isLoggedIn to the below:
#Published var isLoggedin = false {
didSet {
didChange.send(self)
}
The second is how to actually use Environmental objects. This isn't really wrong in Gene's answer, I just noticed a lot of questions about it in the comments and I don't have enough karma to respond to them. Add this to your SceneDelegate view:
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).
var userAuth = UserAuth()
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView().environmentObject(userAuth)
Now you need to just simply create an instance of the new View you want to navigate to and put that in NavigationButton:
NavigationButton(destination: NextView(), isDetail: true, onTrigger: { () -> Bool in
return self.done
}) {
Text("Login")
}
If you return true onTrigger means you successfully signed user in.