SwiftUI authentication view - swift

In swift UI I want the content view to be my root view for my app with a conditional setup to check if the users is logged in or not. If the user is logged in a list view shows other wise show the login view so the user can log in. Based on my research i could not find a best way to do this.
In my case I can not get the solution I found to work and do not know if it is the best solution.
import SwiftUI
struct ContentView: View {
#ObservedObject var userAuth: UserAuth = UserAuth()
// MARK: - View
#ViewBuilder
var body: some View {
if !userAuth.isLoggedin {
return LoginView().environmentObject(userAuth)
}
return BookList()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
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
}
func logout() {
// login request... on success:
self.isLoggedin = false
}
var isLoggedin = false {
didSet {
didChange.send(self)
}
// willSet {
// willChange.send(self)
// }
}
}
When running this all i get is a white screen. It seems that the view builder might be the problem but removing that i get a opaque error on content view

There two problems with provided code snapshot: 1) incorrect view builder content, and 2) incorrect model.
See below both fixed. Tested with Xcode 11.4 / iOS 13.4
struct ContentView: View {
#ObservedObject var userAuth: UserAuth = UserAuth()
// MARK: - View
#ViewBuilder // no need return inside
var body: some View {
if !userAuth.isLoggedin {
LoginView().environmentObject(userAuth)
}
else {
BookList()
}
}
}
import Combine
class UserAuth: ObservableObject {
#Published var isLoggedin = false // published property to update view
func login() {
// login request... on success:
self.isLoggedin = true
}
func logout() {
// login request... on success:
self.isLoggedin = false
}
}

how about just this:
var body: some View {
if !userAuth.isLoggedin {
LoginView().environmentObject(userAuth)
} else {
BookList()
}
}

Related

The view does not show up

I would like to show a ProgressView when the Button is pressed and the user.showLoginProgress property is set, which is declared as published. However, the new value of the property does not seem to trigger the LoginView to refresh, so the ProgressView does not show up:
struct LoginView: View {
#EnvironmentObject var user : User
var body: some View {
VStack{
Button("Login", action: {
user.credentials.showLoginProgress=true // #Published
})
if user.credentials.showLoginProgress{
ProgressView() // never shows up
}
}
}
}
The user is global:
var user : User = User()
#main
struct app1App: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
MainView().environmentObject(user)
}
}
}
and is defined as:
class User : ObservableObject{
#Published var credentials = Login()
}
while the credentials is:
class Login : ObservableObject {
#Published var showLoginProgress : Bool = false
}
Could anyone point out what's wrong with my code?
Try using something like this approach, to re-structure your User and Login, where
there is only one ObservableObject and the login functions are in it.
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#StateObject var manager = UserManager()
var body: some View {
LoginView().environmentObject(manager)
}
}
struct LoginView: View {
#EnvironmentObject var manager: UserManager
var body: some View {
VStack {
if manager.user.isAuthorised {
Text(manager.user.name + " is logged in")
}
Button("Login", action: {
manager.doLogin()
})
if manager.showLoginProgress {
ProgressView()
}
}
}
}
class UserManager: ObservableObject {
#Published var user = User()
#Published var showLoginProgress = false
#Published var someOtherVar = ""
func doLogin() {
showLoginProgress = true
// ...
// simulated login
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.user.name = "Mickey Mouse"
self.user.isAuthorised = true
self.showLoginProgress = false
}
}
}
struct User: Identifiable, Hashable {
let id = UUID()
var name = ""
var isAuthorised = false
// var credentials: Credentials
// ....
}

SwiftUI: #Environment(\.presentationMode)'s dismiss not working in iOS14

I have a view that shows a sheet for filtering the items in a list. The view has this var:
struct JobsTab: View {
#State private var jobFilter: JobFilter = JobFilter()
var filter: some View {
Button {
self.showFilter = true
} label: {
Image(systemName: "line.horizontal.3.decrease.circle")
.renderingMode(.original)
}
.sheet(isPresented: $showFilter) {
FilterView($jobFilter, categoriesViewModel, jobsViewModel)
}
}
However, in the sheet, I'm trying the following and I can't make the view dismissed when clicking on the DONE button, only on the CANCEL button:
struct FilterView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var categoriesViewModel: CategoriesViewModel
#ObservedObject var jobsViewModel: JobsViewModel
let filterViewModel: FilterViewModel
#Binding var jobFilter: JobFilter
#State private var draft: JobFilter
#State var searchText = ""
init(_ jobFilter: Binding<JobFilter>, _ categoriesViewModel: CategoriesViewModel, _ jobsViewModel: JobsViewModel) {
_jobFilter = jobFilter
_draft = State(wrappedValue: jobFilter.wrappedValue)
self.categoriesViewModel = categoriesViewModel
self.jobsViewModel = jobsViewModel
self.filterViewModel = FilterViewModel()
}
...
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("FilterView.Button.Cancel.Text".capitalizedLocalization) {
presentationMode.wrappedValue.dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("FilterView.Button.Done.Text".capitalizedLocalization) {
let request = Job.defaultRequest()
request.predicate = filterViewModel.buildPredicate(withJobFilterDraft: self.draft)
request.sortDescriptors = [NSSortDescriptor(key: #keyPath(Job.publicationDate), ascending: false)]
jobsViewModel.filteredJobsFetchRequest = request
self.jobFilter = self.draft
presentationMode.wrappedValue.dismiss()
}
}
}
I have also tried with a #Binding like Paul says here but there's no luck.
Is there any workaround, or am I doing something wrong?
Thanks in advance!
EDIT: I've posted the properties of both views, because I think the problem comes from the the line in FilterView self.jobFilter = self.draft.
What I'm trying to do here is create a filter view, and the aforementioned line will be executed when the user presses the DONE button: I want to assign my binding jobFilter in the JobsTab the value of the FilterView source of truth (which is a #State) and probably, since I'm updating the binding jobFilter the FilterView is being shown again even though the $showFilter is false? I don't know to be honest.
EDIT2: I have also tried
``
if #available(iOS 15.0, *) {
let _ = Self._printChanges()
} else {
// Fallback on earlier versions
}
in both `FilterView` and its called `JobTabs` and in both, I get the same result: unchanged
According to your code, I assumed your FilterView() is not a sub view, but an independent view by its own.
Therefore, to make sure "presentationMode.wrappedValue.dismiss()" works, you don't need to create #Binding or #State variables outside the FilerView() for passing the data back and forth between different views. Just create one variable inside your FilterView() to make it works.
I don't have your full code, but I created a similar situation to your problem as below code:
import SwiftUI
struct Main: View {
#State private var showFilter = false
var body: some View {
Button {
self.showFilter = true
} label: {
Image(systemName: "line.horizontal.3.decrease.circle")
.renderingMode(.original)
}
.sheet(isPresented: $showFilter) {
FilterView()
}
}
}
struct FilterView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
NavigationView {
VStack {
Text("Filter View")
}.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Text("cancel")
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Text("okay")
}
}
}
}
}
}
the parameter onDismiss is missing:
.sheet(isPresented: $showFilter, onDismiss: { isPresented = false }) { ... }
View model objects are usually a source of bugs because we don't use those in SwiftUI instead we use structs for speed and consistency. I recommend making an #State struct containing a bool property isSheetPresented and also the data the sheet needs. Add a mutating func and call it on the struct from the button event. Pass a binding to the struct into the sheet's view where you can call another mutating function to set the bool to false. Something like this:
struct SheetConfig {
var isPresented = false
var data: [String] = []
mutating func show(initialData: [String] ) {
isPresented = true
data = initialData
}
mutating func hide() {
isPresented = false
}
}
struct ContentView: View {
#State var config = SheetConfig()
var body: some View {
Button {
config.show(intialData: ...)
} label: {
}
.sheet(isPresented: $config.isPresented) {
FilterView($config)
}
Here is what I did to handle this issue.
Create a new protocol to avoid repeatations.
public protocol UIViewControllerHostedView where Self: View {
/// A method which should be triggered whenever dismiss is needed.
/// - Note: Since `presentationMode & isPresented` are not working for presented UIHostingControllers on lower iOS versions than 15. You must call, this method whenever you want to dismiss the presented SwiftUI.
func dismissHostedView(presentationMode: Binding<PresentationMode>)
}
public extension UIViewControllerHostedView {
func dismissHostedView(presentationMode: Binding<PresentationMode>) {
// Note: presentationMode & isPresented are not working for presented UIHostingControllers on lower iOS versions than 15.
if #available(iOS 15, *) {
presentationMode.wrappedValue.dismiss()
} else {
self.topViewController?.dismisOrPopBack()
}
}
}
Extend UIWindow and UIApplication
import UIKit
public extension UIWindow {
// Credits: - https://gist.github.com/matteodanelli/b8dcdfef39e3417ec7116a2830ff67cf
func visibleViewController() -> UIViewController? {
if let rootViewController: UIViewController = self.rootViewController {
return UIWindow.getVisibleViewControllerFrom(vc: rootViewController)
}
return nil
}
class func getVisibleViewControllerFrom(vc:UIViewController) -> UIViewController {
switch(vc){
case is UINavigationController:
let navigationController = vc as! UINavigationController
return UIWindow.getVisibleViewControllerFrom( vc: navigationController.visibleViewController!)
case is UITabBarController:
let tabBarController = vc as! UITabBarController
return UIWindow.getVisibleViewControllerFrom(vc: tabBarController.selectedViewController!)
default:
if let presentedViewController = vc.presentedViewController {
if let presentedViewController2 = presentedViewController.presentedViewController {
return UIWindow.getVisibleViewControllerFrom(vc: presentedViewController2)
}
else{
return vc;
}
}
else{
return vc;
}
}
}
}
#objc public extension UIApplication {
/// LCUIComponents: Returns the current visible top most window of the app.
var topWindow: UIWindow? {
return windows.first(where: { $0.isKeyWindow })
}
var topViewController: UIViewController? {
return topWindow?.visibleViewController()
}
}
Extend View to handle UIHostingController presented View`
public extension View {
weak var topViewController: UIViewController? {
UIApplication.shared.topViewController
}
}
Finally, use the helper method in your SwiftUI view as follows: -
struct YourView: View, UIViewControllerHostedView {
// MARK: - Properties
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body some View {
Button {
// Note: this part trigger a method in a created protocol above.
dismissHostedView(presentationMode: presentationMode)
} label: {
Text("Tap to dismiss")
}
}
In the sheet try adding this instead.
#Environment(\.dismiss) var dismiss // EnvironmentValue
Button("Some text") {
// Code
dismiss()
}

How to Reuse Same Function in Parent and Child View in SwiftUI with NSManagedObject

Suppose we have a main view MainView and a child view LoginView which perform login function for user. I have chosen CoreData to store some persisted data like username and password and #State in MainView to store non-persisted data like logging-in status.
I also want to support automatic login when the user opens the app. So the login function in LoginView is also used in MainView. My demo code is pasted below.
import SwiftUI
import CoreData
struct MainView: View {
#State private var loggingIn: Bool = false
#State private var token: String? = nil
// `UserInfo` here is a `NSManagedObject` fetched from CoreData
#ObservedObject private var info: UserInfo
init() {
// fetch info from CoreData
}
func logIn() {
await MainActor.run {
self.loggingIn = true
}
let token = await performLoginHttpRequest(cred.username!, cred.password!)
await MainActor.run {
self.loggingIn = false
self.token = token
}
}
var body: some View {
NavigationView {
VStack {
if logging {
ProgressView()
} else if token == nil {
Text("Logged Out")
} else {
Text("Token: \(token!)")
}
NavigationLink("Login") {
LoginView(info: info, logging: $loggingIn)
}
}
}
.task {
await logIn()
}
}
}
struct LoginView: View {
#ObservedObject var info: UserInfo
#Binding var loggingIn: Bool
#Binding var token: String?
func logIn() async {
// basically same as in MainView, but
// `loggingIn` and `token` here is wrapped
// with `#Binding`
}
var body: some View {
// UI to login
}
}
As you can see, the problem here is that I have to write the same logic in MainView and LoginView since I do not know how to share the logic in such configuration.
I have tried some ugly workaround like wrapping the UserInfo in another ObservableObject and sharing it between views. But nested ObservableObject seems not recommended in SwiftUI. Is there any elegant way to implement this?

#ObservedObject not triggering redraw if in conditional

I have the following code:
import SwiftUI
struct RootView: View {
#ObservedObject var authentication: AuthenticationModel
var body: some View {
ZStack {
if self.authentication.loading {
Text("Loading")
} else if self.authentication.userId == nil {
SignInView()
} else {
ContentView()
}
}
}
}
However, the #ObservedObject's changes doesn't seem to trigger the switch to the other views. I can "fix" this by rendering
var body: some View {
VStack {
Text("\(self.authentication.loading ? "true" : "false") \(self.authentication.userId ?? "0")")
}.font(.largeTitle)
ZStack {
if self.authentication.loading {
Text("Loading")
} else if self.authentication.userId == nil {
SignInView()
} else {
ContentView()
}
}
}
and suddenly it starts working. Why does #ObservedObject not seem to trigger a rerender if the watched properties are only used in conditionals?
The code for AuthenticationModel is:
import SwiftUI
import Combine
import Firebase
import FirebaseAuth
class AuthenticationModel: ObservableObject {
#Published var userId: String?
#Published var loading = true
init() {
// TODO: Properly clean up this handle.
Auth.auth().addStateDidChangeListener { [unowned self] (auth, user) in
self.userId = user?.uid
self.loading = false
}
}
}
I think the problem could be that you aren't creating an instance of AuthenticationModel.
Can you try the following in RootView?:
#ObservedObject var authentication = AuthenticationModel()

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.