My first initial view for my app is a splash screen:
This screen has code to check if an authToken is available, if it's not then it will toggle the showLogin bool, which in turn causes my view to go to the LoginView(). Else if there is a authToken available, then it calls a function which fetches information about the user, stores it in an Observable Object and then sets showMain bool to true, which in turn changes my view to my main screen.
It works, but whenever it changes view, it puts the previous view at the bottom of the new view. I know I should use a navigation link for this but I can't get it to work properly with my logic and this is the closest I've gotten:
This is my code:
struct Splash: View {
let keychain = KeychainSwift()
#EnvironmentObject var user: userData
#State var showLogin = false
#State var showMain = false
var body: some View {
NavigationView{
VStack{
if(showLogin){
LoginView()
}
if(showMain){
Main()
}
Text("Splash Screen")
.onAppear(){
if(checkLogin()){
let authToken = keychain.get("authToken")
getDataAPI().getUserData(authToken: authToken!, completion: { response, error in
print("Starting")
if(error == nil){
let code = response!["code"] as? String
if(code == "100"){
print("Done")
DispatchQueue.main.async {
user.email = response?["email"] as! String
user.uid = response?["uid"] as! String
user.username = response?["username"] as! String
}
showMain.toggle()
}else{
print(error!)
}
}
})
}else{
showLogin.toggle()
}
}
}
}
}
}
Because they all in VStack, ie. one below another. To solve this you have to remove splash view from view hierarchy, explicitly, like
VStack{
if(showLogin){
LoginView()
}
if(showMain){
Main()
}
if !showLogin && !showMain {
Text("Splash Screen")
// ... other your code
}
}
Related
I have a view and I want to load the data from the database from the start of the view, because I want to edit the profile of my user.
So whenever I will start the my view, the data will be loaded.
This is my code but it gives me an exception on the TextField.
struct ProfileView: View {
#State var myUser = User()
var repo = myRepo()
var body: some View {
VStack{
Form{
Section(header: Text("edit the name")){
TextField("Nume produs", text: self.myUser.name) //IT DOES NOT WORK HERE
}
}
}.onAppear(){
getMyUser()
}
func getMyUser(){
Task{
do{
try await repo.getUserProfile().watch(block: {item in
self.myUser = item as! User
})
}catch{
print("error")
}
}
}
}
This just does not work when I put as a TextField
What is the best way to have the data of my object (myUser) right away on the start of the view?
Just init the String as empty and create the #Binding like so:
import SwiftUI
struct ProfileView: View {
#State var myUser = ""
var body: some View {
VStack{
Form{
Section(header: Text("edit the name")){
TextField("Nume produs", text: $myUser) //IT DOES NOT WORK HERE
}
}
}.onAppear(){
getMyUser()
}
}
func getMyUser() {
Task{
do{
try await Task.sleep(nanoseconds: 3_000_000_000)
myUser = "HELLO"
}catch{
print("error")
}
}
}
}
I am creating an application in which a user, based on the permissions he has, can access the various views.
I use this method to constantly check user permissions:
func checkPermission() {
let docRef = self.DatabaseFirestore.collection("Admins").document(phoneNumber)
docRef.getDocument{(document, error) in
guard error == nil else {
return
}
if let document = document, document.exists {
self.controlloAdmin = true
guard let data = document.data() else {
print("Document data was empty.")
return
}
self.permission = data["Permessi"] as? [Bool] ?? []
} else {
self.controlloAdmin = false
self.isRegistred = false
self.access = false
}
}
}
I don't know if it is the most correct function I could use, but it is one of the few that I have found that works.
This is my view:
struct AdministratorPage: View {
#StateObject var administratorManager = AdministratorManager()
// User variables.
#AppStorage("phoneNumber") var phoneNumber: String = "" // User number.
#AppStorage("Access") var access: Bool = false
var body: some View {
administratorManager.checkPermission()
return NavigationView {
HStack {
VStack {
Text("Home")
Text(phoneNumber)
// Button to log out.
Button("Logout", action: {
self.access = false
})
Button("Alert", action: {
administratorManager.message = "Error title!"
administratorManager.message = "Error message!"
administratorManager.isMessage = true
}).alert(isPresented: $administratorManager.isMessage) {
Alert(title: Text(administratorManager.title), message: Text(administratorManager.message),
dismissButton: .default(Text("Ho capito!")))
}
}
}
}
}
}
When I call the "administratorManager.checkPermission()" function and press the "Alert" button the message is displayed, but even if the button is pressed the alert does not disappear. If I don't call this function, everything works.
How can I solve? Can the alert go against firebase? Is there a more suitable method to read only one data?
photo of the screen when it got locked
I ran your code and I saw the behavior you described.
The reason is the function call directly in the body.
If you want to call a function when when you open a view, use the .onAppear function for that specific view. In your case
.onAppear {
administratorManager.checkPermission()
}
The following (worked for me with you code):
struct AdministratorPage: View {
#StateObject var administratorManager = AdministratorManager()
// User variables.
#AppStorage("phoneNumber") var phoneNumber: String = "" // User number.
#AppStorage("Access") var access: Bool = false
var body: some View {
return NavigationView {
HStack {
VStack {
Text("Home")
Text(phoneNumber)
// Button to log out.
Button("Logout", action: {
self.access = false
})
Button("Alert", action: {
administratorManager.message = "Error title!"
administratorManager.message = "Error message!"
administratorManager.isMessage = true
}).alert(isPresented: $administratorManager.isMessage) {
Alert(title: Text(administratorManager.title), message: Text(administratorManager.message),
dismissButton: .default(Text("Ho capito!")))
}
}
}
}
.onAppear {
administratorManager.checkPermission()
}
}
}
UPDATE: add Snapshot listener instead of polling
Your initial approach was doing a kind of polling, it called the function constantly. Please keep in mind, when you do a Firebase request, you will be billed for the documents you get back. If you do the polling, you get the same document multiple times and will be billed for it.
With my above mentioned example in this answer, you just call the function once.
If you now want to get the live updated from Firestore, you can add a snapshot listener. The approach would be:
func checkPermission() {
let docRef = db.collection("Admins").document(phoneNumber).addSnapshotListener() { documentSnapshot, error in //erca nella collezione se c'รจ il numero.
guard error == nil else {
print("ERROR.")
return
}
if let document = documentSnapshot {
self.controlloAdmin = true
guard let data = document.data() else {
print("Document data was empty.")
return
}
self.permission = data["Permessi"] as? [Bool] ?? []
} else {
self.controlloAdmin = false
self.isRegistred = false
self.access = false
}
}
}
Whenever a value changed on that document in Friestore, it'll be changed on your device as well.
Best, Sebastian
In my app, I have a parent view to display a user's profile with a child view to display the user's ratings. Both of these views have a .task modifier that fetches data from a server (userProfile data for the parent and reviews data for the child).
However, I'm running into an issue where if the parent view refreshes for any reason, the subview data resets and loses its state but the .task modifier doesn't fire.
Is this a bug? How can I force the child view to refresh when the parent view refreshes? Do I need to move the viewmodel out of the child view entirely and inject it?
I've reproduced the basic structure of the app below. When the button is clicked, the parent view state changes, causing the child view to reset and lose its state. However, the .task function isn't called.
struct ContentView: View {
var body: some View {
ParentView()
}
}
struct ParentView: View {
#State var parentViewNumber: Int?
func incrementParent () async {
try? await Task.sleep(nanoseconds: 1 * 1_000_000_000)
if let parentViewNumber = parentViewNumber {
self.parentViewNumber = parentViewNumber + 1
}
else {
parentViewNumber = 1
}
}
var body: some View {
VStack {
Text("Parent View Number: \(parentViewNumber ?? 0)")
Button {
Task {
await incrementParent()
}
} label: {
Text("Increment Parent View")
}
if parentViewNumber != nil {
ChildView(parentViewNumber: parentViewNumber)
}
}
.task {
await incrementParent()
}
}
}
struct ChildView: View {
class ChildViewModel: ObservableObject {
#Published var childViewNumber: Int?
init() {
print ("ChildViewModel init called")
}
#MainActor
func incrementChildViewNubmer () async {
print ("incrementChildViewNubmer called")
try? await Task.sleep(nanoseconds: 1 * 1_000_000_000)
if let childViewNumber = childViewNumber {
self.childViewNumber = childViewNumber + 1
}
else {
childViewNumber = 1
}
}
}
#ObservedObject var childViewModel: ChildViewModel
var parentViewNumber: Int?
init(parentViewNumber: Int?) {
print("ChildView init called")
self.parentViewNumber = parentViewNumber
//In the actual app, this ViewModel receives an Id field.
childViewModel = ChildViewModel()
}
var body: some View {
VStack {
Text("ChildView Number: \(childViewModel.childViewNumber ?? 0)")
Text("ParentView Number \(parentViewNumber ?? 0)")
}
.task {
await childViewModel.incrementChildViewNubmer()
}
}
}
In the above example, when the view initially loads, childViewNumber is set to 1 as expected. When the button is clicked, childViewNumber gets set back to nil, and doesn't get updated.
I'm a beginner iOS developer and I have a problem with my first application. I'm using Firebase as a backend for my app and I have already sign in and sing up methods implemented. My problem is with dismissing LoginView after Auth.auth().signIn method finishing. I've managed to do this when I'm using NavigationLink by setting ObservableObject in isActive:
NavigationLink(destination: DashboardView(), isActive: $isUserLogin) { EmptyView() }
It's working as expected: when app ending login process screen is going to next view - Dashboard.
But I don't want to use NavigationLink and creating additional step, I want just go back to Dashboard using:
self.presentationMode.wrappedValue.dismiss()
In this case I don't know how to force app to wait till method loginUser() ends. This is how my code looks now:
if loginVM.loginUser() {
appSession.isUserLogin = true
self.presentationMode.wrappedValue.dismiss()
}
I've tried to use closures but it doesn't work or I'm doing something wrong.
Many thanks!
You want to use a AuthStateDidChangeListenerHandle and #EnvrionmentObject, like so:
class SessionStore: ObservableObject {
var handle: AuthStateDidChangeListenerHandle?
#Published var isLoggedIn = false
#Published var userSession: UserModel? { didSet { self.willChange.send(self) }}
var willChange = PassthroughSubject<SessionStore, Never>()
func listenAuthenticationState() {
handle = Auth.auth().addStateDidChangeListener({ [weak self] (auth, user) in
if let user = user {
let firestoreUserID = API.FIRESTORE_DOCUMENT_USER_ID(userID: user.uid)
firestoreUserID.getDocument { (document, error) in
if let dict = document?.data() {
//Decoding the user, you can do this however you see fit
guard let decoderUser = try? UserModel.init(fromDictionary: dict) else {return}
self!.userSession = decoderUser
}
}
self!.isLoggedIn = true
} else {
self!.isLoggedIn = false
self!.userSession = nil
}
})
}
func logOut() {
do {
try Auth.auth().signOut()
print("Logged out")
} catch let error {
debugPrint(error.localizedDescription)
}
}
func unbind() {
if let handle = handle {
Auth.auth().removeStateDidChangeListener(handle)
}
}
deinit {
print("deinit - seession store")
}
}
Then simply do something along these lines:
struct InitialView: View {
#EnvironmentObject var session: SessionStore
func listen() {
session.listenAuthenticationState()
}
var body: some View {
ZStack {
Color(SYSTEM_BACKGROUND_COLOUR)
.edgesIgnoringSafeArea(.all)
Group {
if session.isLoggedIn {
DashboardView()
} else if !session.isLoggedIn {
SignInView()
}
}
}.onAppear(perform: listen)
}
}
Then in your app file, you'd have this:
InitialView()
.environmentObject(SessionStore())
By using an #EnvironmentObject you can now access the user from any view, furthermore, this also allows to track the Auth status of the user meaning if they are logged in, then the application will remember.
I created an init like this:
init() {
// Show what's new on the update from App Store
if !UserDefaults.standard.bool(forKey: "appStoreUpdateNotification_2.2.0") {
UserDefaults.standard.set(true, forKey: "appStoreUpdateNotification_2.2.0")
}
}
How can I include the sheet there? Until now I was using a button to display it. This is to show the sheet when I update the app to inform the users of changes
You can try setting a #State variable and use it to present a sheet:
struct ContentView: View {
#State var isShowingModal: Bool = false
init() {
if !UserDefaults.standard.bool(forKey: "appStoreUpdateNotification_2.2.0") {
UserDefaults.standard.set(true, forKey: "appStoreUpdateNotification_2.2.0")
_isShowingModal = .init(initialValue: true)
}
}
var body: some View {
Text("")
.sheet(isPresented: $isShowingModal) {
SomeOtherView()
}
}
}