I have a user object, which I would like to store the currently logged in user for, as well as have functions that will allow the user to do things, such as logout, login, etc.
Following various tutorials and plenty of posts on here, I've come up with some code that works well so far when logging in via the login page. Here is that code:
import Foundation
import UIKit
// define the protocol for using this class
protocol LoginProtocol {
func didRecieveLoginResponse(status: Bool, message: String?)
}
class User : APIControllerProtocol {
var delegate: LoginProtocol
var username: String?
var company: String?
var auth_token: String?
// more properties here
// initiate the class, must have a delegate as per the protocol
init(delegate: LoginProtocol, username: String?, school: String?) {
self.delegate = delegate
if(username != nil) {
self.username = username
self.company = company
}
}
// login using the API
func login(password: String) {
print("Logging in as " + username! + " - " + company!)
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
let api = APIController(delegate: self)
var postFields = [String: String]()
postFields["username"] = username
postFields["password"] = password
postFields["company"] = company
api.request("login",postData: postFields)
}
// API results were received, log in the user if appropriate
func didRecieveAPIResults(originalRequest: String,apiResponse: APIResponse) {
dispatch_async(dispatch_get_main_queue(), {
if(apiResponse.status == true) {
self.auth_token = details["auth_token"]
// more properties here
self.save_to_user_defaults()
self.delegate.didRecieveLoginResponse(true, message:nil )
} else {
self.delegate.didRecieveLoginResponse(false, message:apiResponse.message )
}
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
})
}
// save the properties to user defaults
func save_to_user_defaults() {
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setObject(self.username, forKey: "username")
defaults.setObject(self.company, forKey: "company")
defaults.setObject(self.auth_token, forKey: "auth_token")
// more properties here
}
// load the properties from user defaults
func load_from_user_defaults() {
let defaults = NSUserDefaults.standardUserDefaults()
self.username = defaults.objectForKey("username") as? String
self.company = defaults.objectForKey("company") as? String
self.auth_token = defaults.objectForKey("auth_token") as? String
// more properties here
}
}
The next stage for me is logging in the user via NSUserDefaults - my plan to do this is via something like this:
let user = User()
user.load_from_user_defaults()
However I'm not quite sure:
Whether I'm on the right track (this is my first complete swift app)
If I am, where to put the above 2 lines of code (perhaps the app delegate?), such that when the app is opened, the user in NSUserDefaults (if there is one) is logged back in
How to allow the rest of the app access to the user (I'd like to be able to be able to reference user data in my view controllers, e.g. let pointsLabel.text = users.points)
Thank you so much in advance!
Hope, my answer will help in improving your app:
NSUserDefaults is not secure, it can be easily opened and read, both on device & to a Mac. So user defaults is a good place for preferences and config info, however it's not a good for sensitive information, like passwords. Check the link for more details
Yes, it should be in app delegate. Load the user data and present the view accordingly.
Either you can pass user data to view controller or just read from persistent storage. Depends upon your requirement.
Related
I am using Firebase as my backend, when a user signs up I create a new document in my Firestore DB and then start a listener on that document. When a user signs out I want to be able to stop listening for security reasons.
I currently have a standard Session listener class that handles Firebase authentication and detects when a user has logged in / logged out:
SessionListener
class SessionListener : ObservableObject {
#Published var session: User? { didSet { self.didChange.send(self) }}
var carRepo: CarRepository = CarRepository()
var didChange = PassthroughSubject<SessionListener, Never>()
var handle: AuthStateDidChangeListenerHandle?
func listen () {
// monitor authentication changes using firebase
handle = Auth.auth().addStateDidChangeListener { (auth, user) in
if let user = user {
// if we have a user, create a new user model
self.session = User(uid: user.uid,name,email: user.email)
self.carRepo.initialiseUser()
} else {
// if we don't have a user, set our session to nil
self.session = nil
}
}
}
...
//Sign in / Sign up methods below
I also have a repository class to communicate with my Firestore DB (set up listeners etc.)
CarRepository
class CarRepository: ObservableObject {
let db = Firestore.firestore()
#Published var cars = [Car]()
#Published var listener: ListenerRegistration?
init(){
loadData()
}
func detatchListener(){
if(self.listener != nil) {
self.listener!.remove()
}
}
func loadData() {
if(Auth.auth().currentUser?.uid != nil){
self.listener = db.collection("users").document(Auth.auth().currentUser!.uid).collection("cars").addSnapshotListener { (querySnapshot, error) in
if let querySnapshot = querySnapshot {
querySnapshot.documents.compactMap { document in
try? self.cars.append(document.data(as: Car.self)!)
}
}
}
}
}
...
//Add user to DB below
My problem is that I need to access this CarRepository from both the SessionListener (detect when a user logs out so that I can call detachListener(), and I also need to access CarRepository in a viewModel to parse the cars array.
The problem is that this is creating two separate instances of CarRepository (multiple listeners are being set up for the same document, and only the instance was created by SessionListener will have detachListener() called. Instead, I want there to be a single object that all classes/structs have access to.
Have a singleton manager class for the database interaction. In that class, you can have a property of type [String: CarRepository]. The String key is the path to your document. This way you can guarantee only 1 object per path.
Whenever you think of something that needs to be unique and live with your application, think of singleton.
The sample app here https://getstream.io/ios-activity-feed/tutorial/ is working good using the sample Client api key, app id, and token. Really uploads and downloads Posts that are viewable in the dashboard. However, when I change the api key, app id, and token to mine (I submitted the email to an online Nodejs get stream backend to produce the token), though I am able to push a Post and view in the dashboard, the download fails with this error message:
JSON decoding error: The data couldn’t be read because it isn’t in the correct format..
I compare the data in the two dashboards online and noted that Post in both are of the same types.
At first, I really thought that the FlatFeedViewController in the sample app was a fast and clean solution.
I used 100% code from this link.
In AppDelegate:
apiKey: "xxxxxxxx",
appId: "xxxx",
token: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
Client.shared.getCurrentUser(typeOf: GetStreamActivityFeed.User.self) { result in
if result.error == nil, let viewController = self.window?.rootViewController as? ViewController {
viewController.reloadData()
}
}
In the viewcontroller:
class ViewController: FlatFeedViewController<GetStreamActivityFeed.Activity> {
let textToolBar = TextToolBar.make()
let appDelegate = UIApplication.shared.delegate as! AppDelegate
override func viewDidLoad() {
appDelegate.getStream()
if let feedId = FeedId(feedSlug: "timeline") {
let timelineFlatFeed = Client.shared.flatFeed(feedId)
presenter = FlatFeedPresenter<GetStreamActivityFeed.Activity>(flatFeed: timelineFlatFeed, reactionTypes: [.likes, .comments])
}
super.viewDidLoad()
setupTextToolBar()
subscribeForUpdates()
}
func setupTextToolBar() {
textToolBar.addToSuperview(view, placeholderText: "Share something...")
// Enable URL unfurling.
textToolBar.linksDetectorEnabled = true
// Enable image picker.
textToolBar.enableImagePicking(with: self)
textToolBar.sendButton.addTarget(self, action: #selector(save(_:)), for: .touchUpInside)
}
#objc func save(_ sender: UIButton) {
// Hide the keyboard.
view.endEditing(true)
if textToolBar.isValidContent, let presenter = presenter {
textToolBar.addActivity(to: presenter.flatFeed) { result in
print(result) // It will print the added activity or error.
}
}
}
I expected the uploaded Posts to be downloaded in the app. But the decoding error results. The error is thrown in the public final class Activity, specifically in the function: public required init(from decoder: Decoder) throws {
Please help.
I have created a dummy IOS Application to explain my questions well. Let me share it with all details:
There are 2 Pages in this dummy IOS Application: LoginPageViewController.swift and HomepageViewController.swift
Storyboard id values are: LoginPage, Homepage.
There is login button in Login page.
There are 3 labels in Homepage.
App starts with Login page.
And i have a class file: UserDetail.swift
And there is one segue from login page to home page. Segue id is: LoginPage2Homepage
UserDetail.swift file
import Foundation
class UserDetail {
var accountIsDeleted = false
var userGUID : String?
var userAge: Int?
}
LoginPageViewController.swift file
import UIKit
class LoginPageViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func loginButtonPressed(_ sender: UIButton) {
var oUserDetail = UserDetail()
oUserDetail.accountIsDeleted = true
oUserDetail.userAge = 38
oUserDetail.userName = "Dirk Kuyt"
UserDefaults.standard.set(oUserDetail, forKey: "UserCredentialUserDefaults")
UserDefaults.standard.synchronize()
performSegue(withIdentifier: "LoginPage2Homepage", sender: nil)
}
}
HomepageViewController.swift file
import UIKit
class HomepageViewController: UIViewController {
var result_userGUID = ""
var result_userAge = 0
var result_isDeleted = false
#IBOutlet weak var labelUserGuidOutlet: UILabel!
#IBOutlet weak var labelAgeOutlet: UILabel!
#IBOutlet weak var labelAccountIsDeletedOutlet: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
self.setVariablesFromUserDefault()
labelUserGuidOutlet.text = result_userGUID
labelAgeOutlet.text = String(result_userAge)
labelAccountIsDeletedOutlet.text = String(result_isDeleted)
}
func setVariablesFromUserDefault()
{
if UserDefaults.standard.object(forKey: "UserCredentialUserDefaults") != nil
{
// I need a help in this scope
// I have checked already: My UserDefault exists or not.
// I need to check type of the value in UserDefault if UserDefault is exists. I need to show print if type of the value in UserDefault is not belongs to my custom class.
// And then i need to cast UserDefault to reach my custom class's properties: userGUID, userAge, isDeleted
}
else
{
print("there is no userDefault which is named UserCredentialUserDefaults")
}
}
}
My purposes:
I would like to store my custom class sample(oUserDetail) in UserDefaults in LoginPageViewController with login button click action.
I would like to check below in home page as a first task: My UserDefault exists or not ( I did it already)
I would like to check this in home page as a second task: if my UserDefault exists. And then check type of the UserDefault value. Is it created with my custom class? If it is not. print("value of userdefault is not created with your class")
Third task: If UserDefault is created with my custom class. And then parse that value. Set these 3 variables: result_userGUID, result_userAge, result_isDeleted to show them in labels.
I get an error after I click the login button in Login Page. Can't I store my custom class in UserDefaults? I need to be able to store because I see this detail while I am writing it:
UserDefaults.standart.set(value: Any?, forKey: String)
My custom class is in Any scope above. Isn't it?
You can't store a class instance without conforming to NSCoding / Codable protocols
class UserDetail : Codable {
var accountIsDeleted:Bool? // you can remove this as it's useless if the you read a nil content from user defaults that means no current account
var userGUID : String?
var userAge: Int?
}
store
do {
let res = try JSONEncoder().encode(yourClassInstance)
UserDefaults.standard.set(value:res,forKey: "somekey")
}
catch { print(error) }
retrieve
do {
if let data = UserDefaults.standard.data(forKey:"somekey") {
let res = try JSONDecoder().decode(UserDetail.self,from:data)
} else {
print("No account")
}
}
catch { print(error) }
I am getting some user information from Firebase and storing it into singleton. After that every time the value changes I want that the label changes also but it doesn't until I terminate the app and come back in.
How should I update label if value changes in singleton?
I have tab views. In first tab I assign values and in second tab I try to put the values to label.
This is my singleton:
class CurrentUser: NSObject
{
var generalDetails: User = User()/// Consecutive declarations on a line must be separated by ';'
static let sharedInstance = CurrentUser()
fileprivate override init(){
super.init()
}
}
And like this I assign values:
self.databaseRef.child("users").child(user.uid).observeSingleEvent(of: .value) { (snapshot:FIRDataSnapshot) in
guard let firebaseValue = snapshot.value as? [String:AnyObject], let userName = firebaseValue["username"] as? String, let email = firebaseValue["email"] as? String, let reputation = firebaseValue["reputation"] as? Int, let profilePicURL = firebaseValue["profileImageUrl"] as? String
else
{
print("Error with FrB snapshot")//Here
return
}
//Set values
self.currentUser.generalDetails = User(userName: userName, email: email, reputation: reputation, profileImageURL: profilePicURL, uid: user.uid)
}
And if I want to put the value to the label I simply do this(This reputation is the only thing that can change often):
self.reputationLabel.text = String(self.currentUser.generalDetails.reputation)
You can do either of these:-
Communicate between the singleton and your class with delegate-protocol method , fire the delegate method in the class whenever your repo changes and update your label.
Open a different thread in your network link for the user's reputation in the viewController itself:-
override func viewDidLoad() {
super.viewDidLoad()
FIRDatabase.database().reference().child("users").child(FIRAuth.auth()!.currentUser!.uid).child("reputation").observe(.childChanged, with: {(Snapshot) in
print(Snapshot.value!)
//Update yor label
})
which will get called every time the value of reputation changes.
I like Dravidian's answer and I would like to offer an alternative: KVO
We use Key-Value Observing to monitor if our app is disconnected from the Firebase server. Here's the overview.
We have a singleton which stores a boolean variable isConnected, and that variable is set by observing a special node in Firebase
var isConnected = rootRef.child(".info/connected")
When connected/disconnected, the isConnected var changes state.
We have a little icon on our views that indicates to the user the connected state; when connected it's green, when disconnected it's red with a line through it.
That icon is a class and within each class we have code that observes the isConnected variable; when it's state changes all of the icons change automatically.
It takes very little code, is reusable, is clean and easily maintained.
Here's a code snippet from the Apple documentation
//define a class that you want to observe
class MyObjectToObserve: NSObject {
dynamic var myDate = NSDate()
func updateDate() {
myDate = NSDate()
}
}
//Create a global context variable.
private var myContext = 0
//create a class that observes the myDate var
// and will be notified when that var changes
class MyObserver: NSObject {
var objectToObserve = MyObjectToObserve()
objectToObserve.addObserver(self,
forKeyPath: "myDate",
options: .new,
context: &myContext)
There will be more to it but that's it at a 10k foot level.
The Apple documentation is here
Using Swift with Cocoa and Obj-c 3.01: Design Patterns
and scroll down the the Key-Value Observing Section. It's a good read and very powerful. It follows the same design pattern as Firebase (or vice-versa) - observe a node (variable) and tell me when it changes.
I am logging in my users using Parse. As my app opens my LaunchVieWController determines whether users are already signed in or not:
override func viewDidLoad() {
super.viewDidLoad()
//Make sure cached users don't have to log in every time they open the app
var currentUser = PFUser.currentUser()
println(currentUser)
if currentUser != nil {
dispatch_async(dispatch_get_main_queue()) {
self.performSegueWithIdentifier("alreadySignedIn", sender: self)
}
} else {
dispatch_async(dispatch_get_main_queue()) {
self.performSegueWithIdentifier("showSignUpIn", sender: self)
}
}
}
If users are already signed in they are taken to the table view described below. If they are not logged in, they are taken to a view in which they can go to the signup view or the login view.
My signup works perfectly fine, and signs up users and then redirects them to the table view.
Here is the signup function (it is in a separate controller from the login function):
func processSignUp() {
var userEmailAddress = emailAddress.text
var userPassword = password.text
// Ensure username is lowercase
userEmailAddress = userEmailAddress.lowercaseString
// Add email address validation
// Start activity indicator
activityIndicator.hidden = false
activityIndicator.startAnimating()
// Create the user
var user = PFUser()
user.username = userEmailAddress
user.password = userPassword
user.email = userEmailAddress
user.signUpInBackgroundWithBlock {
(succeeded: Bool, error: NSError?) -> Void in
if error == nil {
dispatch_async(dispatch_get_main_queue()) {
self.performSegueWithIdentifier("signInToNavigation", sender: self)
}
} else {
self.activityIndicator.stopAnimating()
if let message: AnyObject = error!.userInfo!["error"] {
self.message.text = "\(message)"
}
}
}
}
My login function looks like so:
#IBAction func signIn(sender: AnyObject) {
var userEmailAddress = emailAddress.text
userEmailAddress = userEmailAddress.lowercaseString
var userPassword = password.text
PFUser.logInWithUsernameInBackground(userEmailAddress, password:userPassword) {
(user: PFUser?, error: NSError?) -> Void in
if user != nil {
dispatch_async(dispatch_get_main_queue()) {
self.performSegueWithIdentifier("signInToNavigation", sender: self)
}
} else {
if let message: AnyObject = error!.userInfo!["error"] {
self.message.text = "\(message)"
}
}
}
}
After users log in they are sent to a table view. In that table view I am trying to access the current user object in the following function, as I want to load different data based on who the user is:
override func queryForTable() -> PFQuery {
var query = PFQuery(className:"MyClass")
query.whereKey("userID", equalTo: PFUser.currentUser()!)
return query
}
When I try to log in with a user I get thrown the following error: "fatal error: unexpectedly found nil while unwrapping an Optional value". It seems like the PFUser.currentUser() object is nil.
When users are sent to the table view after signing up the PFUser.currentUser() object is set and works perfectly.
My guess is that this is because of the fact that the PFUser.logInWithUsernameInBackground is happening in the background and that my query is trying to get the PFUser.currentUser() object before it has been loaded. Is there anyway I can work around this issue? In the table view the value of PFUser.currentUser() is needed to load the table data. Can I somehow make sure that PFUser.currentUser() gets assigned with the current user object before the function gets called (for example, by not loading in users in the background thread)?
All help is much appreciated!
EDIT: I've updated this post with some more of my code to help highlight any bug that I may be missing.
I discovered that this problem appeared because the segue signInToNavigation was wired from the Login-button, instead from the login view controller itself. This resulted in the segue being executed before the login function was executed, and therefore the PFUser.currentUser()object was not yet assigned when the table view loaded. I solved this by rewiring the segues. Stupid slip-up on my side.
Thanks to Ryan Kreager and Wain for taking time to help me figure out this issue!
You can try checking if PFUser.currentUser() != nil instead to make sure it's being set right after login. If it's not being set right off the bat, you know there is a deeper login problem.
Also, try removing the dispatch_async(dispatch_get_main_queue()){ wrapper around your call to the segue.
It's unnecessary (logInWithUsernameInBackground already returns to the main thread) and I have a hunch that it's creating a racing condition where the local object is not being set first because Parse can't do any post-call cleanup since you're going right for the main thread.