SwiftUI Updating EnvironmentObject outside the view - ios

I have a model called UserData which is used only for the current logged in user. It saves data as profile data, user id, reference to Firestore document, etc.
I'm injecting this as an #EnvironmentObject so I can have access to it everywhere.
final class UserData: ObservableObject {
#Published var profile = Profile.default
#Published var loggedIn: Bool = Auth.auth().currentUser != nil ? true : false
#Published var uid: String = ""
#Published var documentReference: DocumentReference = Firestore.firestore().document("")
}
I also have a UserViewModel with methods on it.
class UserViewModel: ObservableObject {
func login(email: String, password: String, completion: #escaping (Response) -> Void) {
AuthService().login(email: email, password: password) { (response: Response) in
// let error = response.error
completion(response)
}
}
func register(email: String, password: String, firstName: String, lastName: String, completion: #escaping (Response) -> Void) {
AuthService().register(email: email, password: password, firstName: firstName, lastName: lastName) { (response: Response) in
// let error = response.error
completion(response)
}
}
func logout() {
try? Auth.auth().signOut()
}
}
The issue arrives in the View
#EnvironmentObject var userData: UserData
var userViewModel = UserViewModel()
func login() {
if self.email == "" || self.password == "" {
self.error = "You must provide both an email and password"
} else {
self.userViewModel.login(email: self.email, password: self.password) { response in
self.userData.profile = response.profile!
self.userData.uid = response.uid
self.userData.documentReference = response.documentReference ?? Firestore.firestore().document("")
UserDefaults.standard.set(response.userDefaults, forKey: "userDefaults")
}
}
}
As you can see I can not update this UserModel so easily. I have to update each of its properties one by one and also for the registration I have to do the same thing. Same thing in the SceneDelegate. I have the same lines in three different places, login view, registration view, and SceneDelegate
self.userData.profile = response.profile!
self.userData.uid = response.uid
self.userData.documentReference = response.documentReference ?? Firestore.firestore().document("")
UserDefaults.standard.set(response.userDefaults, forKey: "userDefaults")
Is there a better way to handle such things? Like having a single place to update this #EnvironmentObject. They are really cool to use them in the view but updating them outside the view is a real pain. Also It doesn't work well with the UserViewModel since I can not pass UserData to the UserViewModel.
How can I properly use this #EnvironmentObject called UserData

Related

How can I display name of User object in ProfileViewController

I need to display user.username on Text in ProfileView but I got error when I try to fill current user with User. I have to get User in currentUser var.
import Foundation
import FirebaseAuth
import Firebase
class AuthViewModel: ObservableObject
{
#Published var userSession: FirebaseAuth.User?
#Published var currentUser: User?
private var tempUserSession: FirebaseAuth.User?
private let service = UserService()
init()
{
self.userSession = Auth.auth().currentUser
}
func login(withEmail email: String, password: String)
{
Auth.auth().signIn(withEmail: email, password: password) { authResult, error in
if let e = error
{
print(e.localizedDescription)
}
else
{
guard let user = authResult?.user else {return}
self.userSession = user
guard let uid = self.userSession?.uid else { return }
self.service.fetchUser(withUid: uid)
print("Did User log IN")
}
}
}
func fetchUser()
{
guard let uid = self.userSession?.uid else { return }
service.fetchUser(withUid: uid) { user in <----- HERE I GOT AN ERROR Extra trailing closure passed in call
self.currentUser = user
}
}
}
import Foundation
import UIKit
import FirebaseAuth
import SwiftUI
class ProfileViewController: UIViewController
{
var authViewModel = AuthViewModel()
#IBOutlet weak var userNameLabel: UILabel!
override func viewDidLoad()
{
navigationItem.hidesBackButton = true
userNameLabel.text = authViewModel.currentUser?.username // HERE I WANT TO DISPLAY CURRENT USER - USERNAME
print(userNameLabel.text)
}
}
I tried fill currentuser with user but nothings worked. Still in profile view controller I got nill. (currentUser.username = nil)
The code calls fetchUser like it's an asynchronous function and it's not; it's a synchronous function that does not return a value, nor has an escaping completion handler. So that's the cause of the error.
Here's how I would do it. Start with a a simple user class
class MyUser {
var userName = ""
var uid = ""
}
and then a simplified fetchUser using async/await
func fetchUser() {
Task {
let uid = "uid_0"
let foundUser = await self.getUserAsync(withUid: "uid_0")
print(foundUser.userName)
}
}
and then the code to fetch the user from Firestore, instantiate a MyUser object and return it
func getUserAsync(withUid: String) async -> MyUser {
let usersCollection = self.db.collection("users") //self.db points to my firestore
let thisUserDoc = usersCollection.document(withUid)
let snapshot = try! await thisUserDoc.getDocument()
let user = MyUser()
user.userName = snapshot.get("userName") as? String ?? "No Name"
user.uid = withUid
return user
}

Swift Firebase Processing A Custom Object

I am trying to store a struct called 'UnlockingCharacters' in the users document on firebase. I have a struct called 'Character'. When a user taps "unlock" on a character, the 'Character' is added to 'UnlockingCharacters'. I need to store this on firebase in the users document but am struggling to do this.
I have managed to add a 'Character' to 'UnlockingCharacters' and display them in the users profile however it is not stored in firebase so when the app is closed, the 'Character' is no longer in 'UnlockingCharacters'
Here are my structs & classes:
struct Character: Identifiable, Codable {
#DocumentID var id: String?
var character_name: String
var character_type: String
var character_image: String
var character_details: String
var character_usersUnlocking: Int
var character_totalPoints: Int
var user: UserModel?
var didUnlock: Bool? = false
// To identify whether it is being unlocked...
var isUnlocking: Bool = false
}
struct UnlockingCharacters: Identifiable, Codable {
var id = UUID().uuidString
var character: Character
}
class SharedDataModel: ObservableObject {
// Unlocking Characters...
#Published var unlockingCharacters: [Character] = []
}
My functions:
func isUnlocked() -> Bool {
return sharedData.unlockingCharacters.contains { characterData in
return self.characterData.id == characterData.id
}
}
func addToUnlocking() {
if let index = sharedData.unlockingCharacters.firstIndex(where: {
characterData in
return self.characterData.id == characterData.id
}){
// Remove from unlocking...
sharedData.unlockingCharacters.remove(at: index)
}
else {
// Add to unlocking...
sharedData.unlockingCharacters.append(characterData)
}
}
And my UserModel:
struct UserModel: Identifiable, Codable {
var username : String
var pic : String
var bio: String
var uid : String
var id: String { uid }
var activeUnlockingCharacters: [UnlockingCharacters]
}
When trying to process the custom object I get errors:
let ref = Firestore.firestore()
func fetchUser(uid: String,completion: #escaping (UserModel) -> ()){
let db = Firestore.firestore()
ref.collection("Users").document(uid).getDocument { (doc, err) in
guard let user = doc else{return}
let username = user.data()?["username"] as? String ?? "No Username"
let pic = user.data()?["imageurl"] as? String ?? "No image URL"
let bio = user.data()?["bio"] as? String ?? "No bio"
let uid = user.data()?["uid"] as? String ?? ""
do {
try db.collection("Users").document("\(uid)").setData(from: UnlockingCharacters)
} catch let error {
print("Error writing object to Firestore: \(error)")
}
DispatchQueue.main.async {
completion(UserModel(username: username, pic: pic, bio: bio, uid: uid, activeUnlockingCharacters: UnlockingCharacters))
}
}
}
I also get errors in the following line inside my ProfileViewModel:
#Published var userInfo = UserModel(username: "", pic: "", bio: "", uid: "", activeSupportingCharities: [SupportingCharities])
The errors:
Missing argument for parameter 'activeUnlockingCharacters' in call
Cannot convert value of type '[UnlockingCharacters].Type' to expected argument type '[UnlockingCharacters]'
Here is my data structure in the firebase console:
I want there to be a field called UnlockingCharacters in the users data model on firebase when a character is added to the UnlockingCharacters struct.
I think the issue is that your code for writing back to the User document doesn't refer to an instance of UnlockingCharacters , but instead to the type UnlockingCharacters.
So this line:
try db.collection("Users").document("\(uid)").setData(from: UnlockingCharacters)
should probably(*) become
let userModel = UserModel(username: username, pic: pic, bio: bio, uid: uid, activeUnlockingCharacters: unlockedCharacters)
try db.collection("Users").document("\(uid)").setData(from: userModel)
*: probably, because I wasn't sure about your data structure. You might want to post a screenshot of your Firestore data model (in the console) to make it easier to understand how you're intending to store this data.
Also, two other notes:
You probably want to use Codable to replace the manual mapping (let username = user.data()?["username"] as? String ?? "No Username" etc.)
no need to wrap the UI update in DispatchQueue.main.async - Firestore calls back on the main thread already - see https://twitter.com/peterfriese/status/1489683949014196226 .

Vapor 4 authentication

Hey I'm having some problems with the login controllers.My code is:
func login(_ req: Request) throws -> EventLoopFuture<UserToken>{
let user = try req.auth.require(User.self)
let token = try user.generateToken()
return token.save(on: req.db).map { token }
}
But I don't really know that how the function work in postman.This is my usermodel :
import Foundation
import Fluent
import Vapor
import FluentPostgresDriver
final class User:Model,Content{
static let schema = "user"
#ID(key: .id)
var id:UUID?
#Field(key:"帳號")
var account:String
#Field(key: "密碼")
var password:String
init() {}
init(id: UUID?=nil, account:String, password:String){
self.id=id
self.account=account
self.password=password
}
}
extension User: ModelAuthenticatable {
// 要取帳號的欄位
static var usernameKey: KeyPath<User, Field<String>> = \User.$account
// 要取雜湊密碼的欄位
static var passwordHashKey: KeyPath<User, Field<String>> = \User.$password
// 驗證
func verify(password: String) throws -> Bool {
try Bcrypt.verify(password, created: self.password)
}
}
extension User {
struct Create: Content {
var account: String
var password: String
var confirmPassword: String // 確認密碼
}
}
extension User.Create: Validatable {
static func validations(_ validations: inout Validations) {
validations.add("account", as: String.self, is: .count(10...10))
// password需為8~16碼
validations.add("password", as: String.self, is: .count(8...16))
}
}
extension User {
func generateToken() throws -> UserToken {
// 產生一組新Token, 有效期限為一天
let calendar = Calendar(identifier: .gregorian)
let expiryDate = calendar.date(byAdding: .day, value: 1, to: Date())
return try UserToken(value: [UInt8].random(count: 16).base64, expireTime: expiryDate, userID: self.requireID())
}
}
And this is my usertoken:
import Foundation
import Vapor
import Fluent
final class UserToken: Content, Model {
static let schema: String = "user_tokens"
#ID(key: .id)
var id: UUID?
#Field(key: "value")
var value: String
// oken過期時間
#Field(key: "expireTime")
var expireTime: Date?
// 關聯到User
#Parent(key: "user_id")
var user: User
init() { }
init(id: UUID? = nil, value: String, expireTime: Date?, userID: User.IDValue) {
self.id = id
self.value = value
self.expireTime = expireTime
self.$user.id = userID
}
}
extension UserToken: ModelTokenAuthenticatable {
//Token的欄位
static var valueKey = \UserToken.$value
//要取對應的User欄位
static var userKey = \UserToken.$user
// 驗證,這裡只檢查是否過期
var isValid: Bool {
guard let expireTime = expireTime else { return false }
return expireTime > Date()
}
}
While I'm typing the value of "account","password" and "confirmPassword", but it kept telling me that "User not authenticated." ,which I've already have the value in my database.
enter image description here
And I'm sure that the password was right. Is there anything that I missed? I'm pretty new in vapor.
And I followed the article below: https://ken-60401.medium.com/vapor-4-authentication-server-side-swift-1f96b035a117
I think the tutorial linked uses HTTP Basic authentication for the login route and I'm guessing that's the case judging by the code shown (it would be good to show how you're registering the login route).
If that's the case then you need to send the username and password in the request as basic authentication credentials in the Authorization header. The value should be Basic <Credentials> where Credentials is username:password Base 64 encoded. However you can get Postman to do it for you

How to conditionally load view upon app launch in SwiftUI?

I'm currently trying to implement an auto-login feature to my app using UserDefaults. What I would like to do before loading any view is get the UserDefaults email and password and call the login function from my API. If successful, go to Home view, else go to LoginView. My apologies, I'm very new to Swift and on a tight schedule with my project. Here is my code segment. I'm not sure where I can add my logic:
import SwiftUI
#main
struct MyApp: App {
init() {
let email = UserDefaults.standard.string(forKey: "email");
let pw = UserDefaults.standard.string(forKey: "pw");
let api = MyAppAPI()
api.signInUser(email: email, password: pw) { result in
//JSON response contains an 'isError' field
let isError = result.value(forKey: "error") as! Bool
if !isError {
//successful login - what to do from here?
}
}
}
var body: some Scene {
WindowGroup {
LoginView()
}
}
}
Here is a simple way of doing this, you can do this onAppear
import SwiftUI
struct ContentView: View {
let email: String
let pass: String
init() {
self.email = UserDefaults.standard.string(forKey: "email") ?? ""
self.pass = UserDefaults.standard.string(forKey: "pw") ?? ""
}
#State private var result: Bool?
var body: some View {
Group {
if let unwrappedResult: Bool = result {
if unwrappedResult {
Text("Home View, Welcome!")
}
else {
Text("Wrong User or Pass, try again!")
}
}
else {
Text("loading...")
}
}
.onAppear() { loginFunction(email: email, pass: pass) { value in result = value } }
}
}
func loginFunction(email: String, pass: String, completion: #escaping (Bool) -> Void) {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.milliseconds(3000)) { completion(Bool.random()) }
}

Add a spinner on making a moya request using RxSwift and mvvm and dismiss it when user receives a response

I have an app where I am trying to implement RxSwift using MVVM.
I have the SignInViewModel where I am doing the validation and I am updating the login observable with the rest response boolean that I am listening to .
In the controller class when ever the validations pass the login button gets enabled.
In a similar manner I want to be able to start a spinner on click of the button and dismiss when the user receives a response.
When I try to listen to the loginObservable in from view model in the controller class. it does not hit the bind block.
I am not able to figure out what the problem is.
Any help will be appreciated
Following is my SignInViewModel
class SignInViewModel {
let validatedEmail: Observable<Bool>
let validatedPassword: Observable<Bool>
let loginEnabled: Observable<Bool>
let loginObservable: Observable<Bool>
init(username: Observable<String>,
password: Observable<String>,
loginTap: Observable<Void>) {
self.validatedEmail = username
.map { $0.characters.count >= 5 }
.shareReplay(1)
self.validatedPassword = password
.map { $0.characters.count >= 2 }
.shareReplay(1)
self.loginEnabled = Observable.combineLatest(validatedEmail, validatedPassword ) { $0 && $1 }
let userAndPassword = Observable.combineLatest(username, password) {($0,$1)}
self.loginObservable = loginTap.withLatestFrom(userAndPassword).flatMapLatest{ (username, password) in
return RestService.login(username: username, password: password).observeOn(MainScheduler.instance)
}
}
}
Following is the moyaRequest class
final class MoyaRequest{
func signIn(userData: Creator) -> Observable<Response> {
return provider.request(.signIn(userData))
.filter(statusCode: 200)
}
}
Following is my RestService class
class RestService:NSObject {
static var moyaRequest = MoyaRequest()
static var disposeBag = DisposeBag()
static func login(username: String, password: String) -> Observable<Bool> {
let userData = Creator()
userData?.username = username
userData?.password = password
print("Username password", userData?.username, userData?.password)
return Observable.create { observer in moyaRequest.signIn(userData: userData!).subscribe{ event -> Void in
switch event {
case .next(let response):
print("Response",response)
case .error(let error):
let moyaError: MoyaError? = error as? MoyaError
let response: Response? = moyaError?.response
let statusCode: Int? = response?.statusCode
print("Sample Response code error" + String(describing: statusCode))
default:
break
}
}
return Disposables.create()
}
}
}
I am trying to bind the view model in the controller class.
class SignInViewController: UIViewController{
let disposeBag = DisposeBag()
#IBOutlet weak var passwordTextfield: UITextField!
#IBOutlet weak var usernameTextfield: UITextField!
private var viewModel : SignInViewModel!
#IBOutlet weak var signInButton: UIButton!
override func viewDidLoad() {
setUpRxViewModel()
}
func setUpRxViewModel(){
self.viewModel = SignInViewModel(username: self.usernameTextfield.rx.text.orEmpty.asObservable(),
password: self.passwordTextfield.rx.text.orEmpty.asObservable(),
loginTap: self.signInButton.rx.tap.asObservable())
self.viewModel.loginEnabled.bind{ valid in
self.signInButton.isEnabled = valid
}.addDisposableTo(disposeBag)
self.viewModel.loginObservable.bind{ input in
print("Login Clicked")
}.addDisposableTo(disposeBag)
}
}
In your login method you are not dispatching any events to your observer. It should be:
case .next(let response):
observer.on(.next(true))
print("Response",response)
case .error(let error):
observer.on(.error(error))
//or observer.on(.next(false)) if you intend to use Bool as indicator of operation success which is a very bad idea.
let moyaError: MoyaError? = error as? MoyaError
let response: Response? = moyaError?.response
let statusCode: Int? = response?.statusCode
furthermore I recommend you use RxMoyaProvider everywhere if you are using Moya with RxSwift. Using Observable.create usually means you are doing something wrong.
You also shouldn't filter off events based on status code at the level of network request because if something goes wrong you are not going to receive any event in your chain.

Resources