Singleton variable turns empty after switching views - swift

I made a singleton class to access my user's email from any other class for easy upload of data to cloud. I am able to print the email in the first view but when i try to access it in other views or come back to this view, it shows up as empty.
class SingletonAccount: NSObject {
static var shared: SingletonAccount = SingletonAccount()
var userEmail: String = "ABCD"
}
I also set the singleton to be initialised only in the appDelegate so that it could be shared among all the other classes.
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var userAccount: SingletonAccount?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
userAccount = SingletonAccount.shared
return true
}
Below is the first view controller that appears on launch
weak var userAccount: SingletonAccount?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.navigationBar.isHidden = true
print(userAccount!.userEmail)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
}
override func viewDidLoad() {
super.viewDidLoad()
let tap = UITapGestureRecognizer(target: self, action: #selector(LoginViewController.tapRegister))
let appDelegate = UIApplication.shared.delegate as! AppDelegate
userAccount = appDelegate.userAccount
self.registerLbl.addGestureRecognizer(tap)
// Do any additional setup after loading the view.
}

The benefit of a singleton is to have access to the same instance from everywhere.
Declaring a property in AppDelegate is pointless. Just get the mail address with
let email = SingletonAccount.shared.userEmail
And declare shared as constant. The description Singleton implies a constant.
static let shared = SingletonAccount()
And it's not necessary that the class inherits from NSObject, it's not even necessary that the singleton is a class.

you're declaring a new instance of userAccount in your view controller. if you have to use the var in appDelegate do
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let aVariable = appDelegate.userAccount
i would just always use SingletonAccount.shared. Or define you singleton globally outside of the SingletonAccount class
let gSingletonAccount: SingletonAccount = SingletonAccount()
class SingletonAccount: NSObject {
var userEmail: String = "ABCD"
}

Related

Unable to access property of UITableView subclass

I am trying to test whether the method ReloadData() is called by an instance of UITableView when it's dataSource is updated.
I've created a subclass of UITableView called MockTableView. It has a bool called reloadDataGotCalled which is set to true when the overridden function reloadData() is called. I then try access that property from within my test class to test whether it is true.
However when I try to do so the compiler gives me the message that "Value of type 'UITableView' has no member 'reloadDataGotCalled'"
I'm not sure why it's doing that, because as far as I can see I've set that value to be of the type 'MockTableView' which should have that member?
// A ViewController that contains a tableView outlet that I want to test.
class ItemListViewController: UIViewController {
let itemManager = ItemManager()
#IBOutlet var tableView: UITableView!
#IBOutlet var dataProvider: (UITableViewDataSource & UITableViewDelegate & ItemManagerSettable)!
#IBAction func addItem(_ sender: UIBarButtonItem) {
if let nextViewController = storyboard?.instantiateViewController(identifier: "InputViewController") as? InputViewController {
nextViewController.itemManager = itemManager
present(nextViewController, animated: true, completion: nil)
}
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = dataProvider
tableView.delegate = dataProvider
dataProvider.itemManager = itemManager
}
}
// My test class
class ItemListViewControllerTest: XCTestCase {
var sut: ItemListViewController!
override func setUp() {
//Given
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let viewController = storyboard.instantiateViewController(identifier: "ItemListViewController")
sut = (viewController as! ItemListViewController)
//When
sut.loadViewIfNeeded()
}
// The test where I'm trying to assign sut.tableView to mockTableView
func test_TableView_IsReloadedWhenItemAddedToItemManger() {
let mockTableView = MockTableView()
sut.tableView = mockTableView
let item = ToDoItem(title: "Foo")
sut.itemManager.add(item)
sut.beginAppearanceTransition(true, animated: true)
sut.endAppearanceTransition()
XCTAssertTrue(sut.tableView.reloadDataGotCalled) // <- this is where I'm getting the compiler message "Value of type 'UITableView' has no member 'reloadDataGotCalled'"
}
}
// My mockTableView subclass in an extension of the ItemListViewControllerTests
extension ItemListViewControllerTest {
class MockTableView: UITableView {
var reloadDataGotCalled = false
override func reloadData() {
super.reloadData()
reloadDataGotCalled = true
}
}
}
I'm expecting that it should compile, and then the test should fail because I've not written the code to make it pass yet?
You have defined tableView instance in ItemListViewController as UITableView. So, you can't access the MockTableView's property with that instance.
You can only access the parent's properties from the children not the vice versa. If you still want to access the property you can try something like the snippet below.
XCTAssertTrue((sut.tableView as! MockTableView).reloadDataGotCalled)
Hope it helps.

Swift: Passing managedObjectContext from AppDelegate to UINavigationController

I have some iOS project (Project) with an #escaping method in the AppDelegate that is used to create the NSPersistentContainer for this project. This method is then called within the didFinishLaunchingWithOptions method as follows:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var persistentContainer: NSPersistentContainer!
func createProjectContainer(completion: #escaping (NSPersistentContainer) -> ()) {
let container = NSPersistentContainer(name: "Project")
container.loadPersistentStores { (_, error) in
guard error == nil else { fatalError("Failed to load store: \(error!)") }
DispatchQueue.main.async { completion(container) }
}
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
createProjectContainer { container in
self.persistentContainer = container
let storyboard = self.window?.rootViewController?.storyboard
guard let vc = storyboard?.instantiateViewController(withIdentifier: "NavigationController") as? NavigationController else {
fatalError("Cannot instantiate root view controller")
}
vc.managedObjectContext = container.viewContext
self.window?.rootViewController = vc
}
return true
}
}
The NavigationController class is simply a subclass of UINavigationController, which embeds a UIViewController (ChildViewController) and has an NSManagedObjectContext variable (managedObjectContext), which I want to pass to the first ChildViewController:
class NavigationController: UINavigationController {
var managedObjectContext: NSManagedObjectContext!
override func viewDidLoad() {
super.viewDidLoad()
guard let vc = self.childViewControllers.first as? ChildViewController else { fatalError("") }
vc.managedObjectContext = managedObjectContext
print("NavigationController: viewDidLoad")
print("managedObjectContext ---> \(managedObjectContext)")
}
}
Now the print("NavigationController: viewDidLoad") appears to be called twice. On the first call, managedObjectContext = nil, on the second call it has been assigned. Does this matter that the app is loading this twice?
It seems to be happening during the storyboard?.instantiateViewController(withIdentifier: "NavigationController") which is called after the first time that NavigationController has been loaded due to the #escaping property of the closure. However, if I exclude that line, as just get the reference to the NavigationController the managedObjectContext appears never to be assigned.

Got delegate nil error even it was set in the main viewcontroller

I have simple app which the welcomeVC shows a greeting message with a person's name. In another VC nameVC, users can change the name they want to be called, and it supposes to update the name on the welcomeVC when they click back.
In order to pass the name from nameVC back to welcomeVC, I set up a protocol ChangeNameDelegate in nameVC, and created a variable in my class:
protocol ChangeNameDelegate {
func updateName(name: String)
}
class nameViewController: UIViewController {
#IBOutlet weak var nameTextField: UITextField!
var changeNameDelegate: ChangeNameDelegate!
override func viewDidLoad() {
}
#IBAction func closeNameVC(_ sender: UIButton) {
if let newName = nameTextField.text {
changeNameDelegate.updateName(name: newName)
}
dismiss(animated: true, completion: nil)
}
}
In my welcomeVC, I have made sure that it register the delegate and conforms to the protocol by setting up as follows:
class welcomeViewController: UIViewController, UIViewControllerTransitioningDelegate {
let nameVC = nameViewController()
override func viewDidLoad() {
nameVC.changeNameDelegate = self
}
}
extension welcomeViewController: ChangeNameDelegate {
func updateName(name: String) {
print("The name has been updated!")
nameLabel.text = name
}
}
However when I ran my app, it got a crash because changeNameDelegate appeared to be nil. Does anybody know what could be missed there? Thanks in advance!
It seems that you are instantiating new instance of NameViewController when routing from WelcomeViewController. You should navigate to nameVC which is instantiated before.
I later solved the issue by using notification/observer.
In the parent view:
let userUpdateNotification = Notification.Name(rawValue: "nameNotificationKey")
NotificationCenter.default.addObserver(self, selector: #selector(updateUser), name: userUpdateNotification, object: nil)
}
func updateUser(not: NSNotification) {
if let userInfo = not.userInfo {
if let userName = userInfo["name"] as? String {
nameLabel.text = userName
}
}
}
In the child view:
let name = Notification.Name(rawValue: "nameNotificationKey")
NotificationCenter.default.post(name: name , object: self, userInfo: ["name": nameTextField.text!] )
}
I don't know if delegate/protocol doesn't work between view controllers that are unconnected. Anyone who knows about this is welcomed to comment!

iOS firebase: FIRAuthUIDelegate.authUI not being called

I am trying to launch google login from AppDelegate.swift and then launch my app's main screen upon login success.
I am able to
show the google login button as shown above
the user is sent to google to sign in
the user is sent back to original (step 1)
After step 3. I'd like to send the user to my app's main page.
My code is below. The problem I'm having is that authUI is not being called.
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, FIRAuthUIDelegate {
var window: UIWindow?
var authUI: FIRAuthUI?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
FIRApp.configure()
authUI = FIRAuthUI.defaultAuthUI()
authUI?.delegate = self
let providers: [FIRAuthProviderUI] = [FIRGoogleAuthUI()]
authUI?.providers = providers
// show google login button
let authViewController = authUI?.authViewController()
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
self.window?.rootViewController = authViewController
self.window?.makeKeyAndVisible()
return true
}
func application(application: UIApplication, openURL url: NSURL, options: [String: AnyObject]) -> Bool {
return GIDSignIn.sharedInstance().handleURL(url, sourceApplication: options[UIApplicationOpenURLOptionsSourceApplicationKey] as? String, annotation: options[UIApplicationOpenURLOptionsAnnotationKey])
}
func authUI(authUI: FIRAuthUI, didSignInWithUser user: FIRUser?, error: NSError?) {
// launch main view controller
}
}
EDIT: This appears to be a duplicate of another question. The other question's title is quite general and only gets to the details a few lines deep. In any case, I believe Chris's answer is more thorough than the one there. I think both the question and answers here are clearer, more pointed and more thorough so it would be a mistake to just direct people here to go there as would happen if this was marked as a duplicate.
I think your problem lies here, in the - (void)signInWithProviderUI:(id<FIRAuthProviderUI>)providerUI method.
The delegate method is called in the dismissViewControllerAnimated:completion: completion block.
[self.navigationController dismissViewControllerAnimated:YES completion:^{
[self.authUI invokeResultCallbackWithUser:user error:error];
}];
As you can see from the Apple docs, this method is expected to be called on a modally presented viewController. You are displaying it as a root view controller. Try displaying it with a modal from a UIViewController, and things should work out. To debug this try and set a breakpoint at line 193 to see that it won't get hit. I would be very surprised if this doesn't work when you display the authController modally.
To come up with a possible solution to your problem (I am assuming you want to ensure a user is signed in before using your app). The below is a simplification of what I am using in an app currently.
EDIT: Updated for the new 1.0.0 FirebaseUI syntax.
class MainTabController: UITabBarController, FIRAuthUIDelegate {
let authUI: FUIAuth? = FUIAuth.defaultAuthUI()
override func viewDidLoad() {
super.viewDidLoad()
var authProviders = [FUIFacebookAuth(), FUIGoogleAuth()]
authUI.delegate = self
authUI.providers = authProviders
//I use this method for signing out when I'm developing
//try! FIRAuth.auth()?.signOut()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !isUserSignedIn() {
showLoginView()
}
}
private func isUserSignedIn() -> Bool {
guard FIRAuth.auth()?.currentUser != nil else { return false }
return true
}
private func showLoginView() {
if let authVC = FUIAuth.defaultAuthUI()?.authViewController() {
present(authVC, animated: true, completion: nil)
}
}
func authUI(_ authUI: FUIAuth, didSignInWith user: FIRUser?, error: Error?) {
guard let user = user else {
print(error)
return
}
...
}
It must be a problem of reference.
class AppDelegate: UIResponder, UIApplicationDelegate, FIRAuthUIDelegate {
var window: UIWindow?
let authUI = FIRAuthUI.defaultAuthUI()
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
FIRApp.configure()
authUI.delegate = self
let providers: [FIRAuthProviderUI] = [FIRGoogleAuthUI()]
authUI.providers = providers
// show google login button
let authViewController = authUI.authViewController()
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
self.window?.rootViewController = authViewController
self.window?.makeKeyAndVisible()
return true
}
}
Try this. AppDelegate will hold the reference of authUI and its delegate.

How to read a variable value in the AppDelegate from a ViewController using Swift

I would like to read the value of a variable contained in the AppDelegate from a ViewController. The variable contains the Device Token used to enable iOS Push Notifications and I would like to show it in a UILabel.
This is my code so far:
AppDelegate.swift
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
internal var deviceTokenToPass: String?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
let pushNotificationsTypes: UIUserNotificationType = UIUserNotificationType.Alert | UIUserNotificationType.Badge | UIUserNotificationType.Sound
let pushNotificationsSettings: UIUserNotificationSettings = UIUserNotificationSettings(forTypes: pushNotificationsTypes, categories: nil)
application.registerUserNotificationSettings(pushNotificationsSettings)
application.registerForRemoteNotifications()
return true
}
func application(application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: NSData) {
let chararacterSet: NSCharacterSet = NSCharacterSet(charactersInString: "<>")
self.deviceTokenToPass = (deviceToken.description as NSString).stringByTrimmingCharactersInSet(chararacterSet).stringByReplacingOccurrencesOfString(" ", withString: "", options: nil, range: nil) as String
}
}
ViewController.swift
import UIKit
class ViewController: UIViewController {
#IBOutlet var deviceTokenLabel : UILabel!
override func viewDidLoad() {
super.viewDidLoad()
let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
var deviceToken = appDelegate.deviceTokenToPass
println(deviceToken) /* nil */
if deviceToken != nil {
self.deviceTokenLabel.text = deviceToken
} else {
self.deviceTokenLabel.numberOfLines = 4
self.deviceTokenLabel.text = "Cannot read your device token.\nYou must be using an iOS Simulator or you didn't allowed the application for push notifications"
}
}
}
The problem is that if I place the println(deviceToken) code in the AppDelegate.swift the device token is correctly displayed in the console, if I place it in the ViewController.swift it's value will be nil.
If you ensure that the property that you'd like to access is actually readable, then you can use the following:
let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
let anAttribute = appDelegate.theAttribute
The didRegisterForRemoteNotificationsWithDeviceToken notification is asynchronous (invoked after the device has been successfully registered), so what I think is happening is that the notification occurs after the view controller is displayed.
To check that, print it from both the app delegate and the view controller, and see which happens first.
As to how fix that, I would implement didFailToRegisterForRemoteNotificationsWithError, so that one of the 2 is invoked, then I would send a notification (via NSNotificationCenter) in both cases (success and failure), and store the outcome in a property.
In the view controller, I would subscribe for the custom notification, and show a progress view until a notification is received. But in viewDidLoad I'd also check for the outcome property set in the app delegate, just in case the registration is too fast and it's already been completed.
in app delegate
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
weak var label: UILabel? = nil
func application(application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: NSData) {
let chararacterSet: NSCharacterSet = NSCharacterSet(charactersInString: "<>")
let deviceTokenToPass = (deviceToken.description as NSString).stringByTrimmingCharactersInSet(chararacterSet).stringByReplacingOccurrencesOfString(" ", withString: "", options: nil, range: nil) as String
if let l = label {
l.text = deviceTokenToPass
}
}
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
let pushNotificationsTypes: UIUserNotificationType = UIUserNotificationType.Alert | UIUserNotificationType.Badge | UIUserNotificationType.Sound
let pushNotificationsSettings: UIUserNotificationSettings = UIUserNotificationSettings(forTypes: pushNotificationsTypes, categories: nil)
application.registerUserNotificationSettings(pushNotificationsSettings)
application.registerForRemoteNotifications()
return true
}
}
in ViewController
class ViewController: UIViewController {
#IBOutlet weak var deviceTokenLabel : UILabel!
override func viewDidLoad() {
super.viewDidLoad()
if let appDelegate = UIApplication.sharedApplication().delegate as? AppDelegate {
appDelegate.label = deviceTokenLabel
}
}
}