Sync data between two viewcontrollers to avoid creating same observer again
First I would like to remind you about the singleton design patter :
In software engineering, the singleton pattern is a software design pattern that restricts the instantiation of a class to one object. This is useful when exactly one object is needed to coordinate actions across the system.
So the first thing you need to do is to create a call that contains as a parameters the data you get from firebase, I have did the following to get the following in order to get the user data when he logged in into my app and then use these data in every part of my application (I don't have any intention to pass the data between VC this is absolutely the wrong approach )
my user class is like this :
import Foundation
class User {
static let sharedInstance = User()
var uid: String!
var username: String!
var firstname: String!
var lastname: String!
var profilePictureData: Data!
var email: String!
}
after that I have created another class FirebaseUserManager (you can do this in your view controller but it's always an appreciated idea to separate your view your controller and your model in order to make any future update easy for you or for other developer )
So my firebaseUserManager class contains something like this
import Foundation
import Firebase
import FirebaseStorage
protocol FirebaseSignInUserManagerDelegate: class {
func signInSuccessForUser(_ user: FIRUser)
func signInUserFailedWithError(_ description: String)
}
class FirebaseUserManager {
weak var firebaseSignInUserManagerDelegate: FirebaseSignInUserManagerDelegate!
func signInWith(_ mail: String, password: String) {
FIRAuth.auth()?.signIn(withEmail: mail, password: password) { (user, error) in
if let error = error {
self.firebaseSignInUserManagerDelegate.signInUserFailedWithError(error.localizedDescription)
return
}
self.fechProfileInformation(user!)
}
}
func fechProfileInformation(_ user: FIRUser) {
var ref: FIRDatabaseReference!
ref = FIRDatabase.database().reference()
let currentUid = user.uid
ref.child("users").queryOrderedByKey().queryEqual(toValue: currentUid).observeSingleEvent(of: .value, with: { snapshot in
if snapshot.exists() {
let dict = snapshot.value! as! NSDictionary
let currentUserData = dict[currentUid] as! NSDictionary
let singletonUser = User.sharedInstance
singletonUser.uid = currentUid
singletonUser.email = currentUserData["email"] as! String
singletonUser.firstname = currentUserData["firstname"] as! String
singletonUser.lastname = currentUserData["lastname"] as! String
singletonUser.username = currentUserData["username"] as! String
let storage = FIRStorage.storage()
let storageref = storage.reference(forURL: "gs://versus-a107c.appspot.com")
let imageref = storageref.child("images")
let userid : String = (user.uid)
let spaceref = imageref.child("\(userid).jpg")
spaceref.data(withMaxSize: 1 * 1024 * 1024) { data, error in
if let error = error {
// Uh-oh, an error occurred!
print(error.localizedDescription)
} else {
singletonUser.profilePictureData = data!
print(user)
self.firebaseSignInUserManagerDelegate.signInSuccessForUser(user)
}
}
}
})
}
}
so basically this class contains some protocols that we would implements and two functions that manager the firebase signIn and fechProfileInformation , that will get the user information
than in my login View controller I did the following :
1 implement the protocol
class LoginViewController: UIViewController, FirebaseSignInUserManagerDelegate
2 in the login button I did the following
@IBAction func loginAction(_ sender: UIButton) {
guard let email = emailTextField.text, let password = passwordTextField.text else { return }
let firebaseUserManager = FirebaseUserManager()
firebaseUserManager.firebaseSignInUserManagerDelegate = self
firebaseUserManager.signInWith(email, password: password)
}
3 implement the protocol method :
func signInSuccessForUser(_ user: FIRUser) {
// Do something example navigate to the Main Menu
}
func signInUserFailedWithError(_ description: String) {
// Do something : example alert the user
}
So right now when the user click on the sign in button there is an object created which contains the user data save on firebase database
now comes the funny part (the answer of your question : how to get the user data in every where in the app)
in every part of my app I could make
print(User.sharedInstance.uid) or print(User.sharedInstance. username)
and I get the value that I want to.
PS : In order to use the singleton appropriately you need to make sure that you call an object when it's instantiated.
Passing data between view controllers
This question seems to be very popular here on Stack Overflow so I thought I would try and give a better answer to help out people starting in the world of iOS like me.
Passing Data Forward
Passing data forward to a view controller from another view controller. You would use this method if you wanted to pass an object/value from one view controller to another view controller that you may be pushing on to a navigation stack.
For this example, we will have ViewControllerA
and ViewControllerB
To pass a BOOL
value from ViewControllerA
to ViewControllerB
we would do the following.
in
ViewControllerB.h
create a property for theBOOL
@property (nonatomic, assign) BOOL isSomethingEnabled;
in
ViewControllerA
you need to tell it aboutViewControllerB
so use an#import "ViewControllerB.h"
Then where you want to load the view, for example, didSelectRowAtIndex
or some IBAction
, you need to set the property in ViewControllerB
before you push it onto the navigation stack.
ViewControllerB *viewControllerB = [[ViewControllerB alloc] initWithNib:@"ViewControllerB" bundle:nil];
viewControllerB.isSomethingEnabled = YES;
[self pushViewController:viewControllerB animated:YES];
This will set isSomethingEnabled
in ViewControllerB
to BOOL
value YES
.
Passing Data Forward using Segues
If you are using Storyboards you are most likely using segues and will need this procedure to pass data forward. This is similar to the above but instead of passing the data before you push the view controller, you use a method called
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
So to pass a BOOL
from ViewControllerA
to ViewControllerB
we would do the following:
in
ViewControllerB.h
create a property for theBOOL
@property (nonatomic, assign) BOOL isSomethingEnabled;
in
ViewControllerA
you need to tell it aboutViewControllerB
, so use an#import "ViewControllerB.h"
Create the segue from
ViewControllerA
toViewControllerB
on the storyboard and give it an identifier. In this example we'll call it"showDetailSegue"
Next, we need to add the method to
ViewControllerA
that is called when any segue is performed. Because of this we need to detect which segue was called and then do something. In our example, we will check for"showDetailSegue"
and if that's performed, we will pass ourBOOL
value toViewControllerB
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
if([segue.identifier isEqualToString:@"showDetailSegue"]){
ViewControllerB *controller = (ViewControllerB *)segue.destinationViewController;
controller.isSomethingEnabled = YES;
}
}
If you have your views embedded in a navigation controller, you need to change the method above slightly to the following
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
if([segue.identifier isEqualToString:@"showDetailSegue"]){
UINavigationController *navController = (UINavigationController *)segue.destinationViewController;
ViewControllerB *controller = (ViewControllerB *)navController.topViewController;
controller.isSomethingEnabled = YES;
}
}
This will set isSomethingEnabled
in ViewControllerB
to BOOL
value YES
.
Passing Data Back
To pass data back from ViewControllerB
to ViewControllerA
you need to use Protocols and Delegates or Blocks, the latter can be used as a loosely coupled mechanism for callbacks.
To do this we will make ViewControllerA
a delegate of ViewControllerB
. This allows ViewControllerB
to send a message back to ViewControllerA
enabling us to send data back.
For ViewControllerA
to be a delegate of ViewControllerB
it must conform to ViewControllerB
's protocol which we have to specify. This tells ViewControllerA
which methods it must implement.
In
ViewControllerB.h
, below the#import
, but above@interface
you specify the protocol.@class ViewControllerB;
@protocol ViewControllerBDelegate <NSObject>
- (void)addItemViewController:(ViewControllerB *)controller didFinishEnteringItem:(NSString *)item;
@endNext still in the
ViewControllerB.h
, you need to set up adelegate
property and synthesize inViewControllerB.m
@property (nonatomic, weak) id <ViewControllerBDelegate> delegate;
In
ViewControllerB
we call a message on thedelegate
when we pop the view controller.NSString *itemToPassBack = @"Pass this value back to ViewControllerA";
[self.delegate addItemViewController:self didFinishEnteringItem:itemToPassBack];That's it for
ViewControllerB
. Now inViewControllerA.h
, tellViewControllerA
to importViewControllerB
and conform to its protocol.#import "ViewControllerB.h"
@interface ViewControllerA : UIViewController <ViewControllerBDelegate>In
ViewControllerA.m
implement the following method from our protocol- (void)addItemViewController:(ViewControllerB *)controller didFinishEnteringItem:(NSString *)item
{
NSLog(@"This was returned from ViewControllerB %@", item);
}Before pushing
viewControllerB
to navigation stack we need to tellViewControllerB
thatViewControllerA
is its delegate, otherwise we will get an error.ViewControllerB *viewControllerB = [[ViewControllerB alloc] initWithNib:@"ViewControllerB" bundle:nil];
viewControllerB.delegate = self
[[self navigationController] pushViewController:viewControllerB animated:YES];
References
- Using Delegation to Communicate With Other View Controllers in the View Controller Programming Guide
- Delegate Pattern
NSNotification center
It's another way to pass data.
// Add an observer in controller(s) where you want to receive data
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleDeepLinking:) name:@"handleDeepLinking" object:nil];
-(void) handleDeepLinking:(NSNotification *) notification {
id someObject = notification.object // Some custom object that was passed with notification fire.
}
// Post notification
id someObject;
[NSNotificationCenter.defaultCenter postNotificationName:@"handleDeepLinking" object:someObject];
Passing Data back from one class to another (A class can be any controller, Network/session manager, UIView subclass or any other class)
Blocks are anonymous functions.
This example passes data from Controller B to Controller A
Define a block
@property void(^selectedVoucherBlock)(NSString *); // in ContollerA.h
Add block handler (listener)
Where you need a value (for example, you need your API response in ControllerA or you need ContorllerB data on A)
// In ContollerA.m
- (void)viewDidLoad {
[super viewDidLoad];
__unsafe_unretained typeof(self) weakSelf = self;
self.selectedVoucherBlock = ^(NSString *voucher) {
weakSelf->someLabel.text = voucher;
};
}
Go to Controller B
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
ControllerB *vc = [storyboard instantiateViewControllerWithIdentifier:@"ControllerB"];
vc.sourceVC = self;
[self.navigationController pushViewController:vc animated:NO];
Fire block
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:
(NSIndexPath *)indexPath {
NSString *voucher = vouchersArray[indexPath.row];
if (sourceVC.selectVoucherBlock) {
sourceVC.selectVoucherBlock(voucher);
}
[self.navigationController popToViewController:sourceVC animated:YES];
}
Another Working Example for Blocks
Swift 3.0 iOS10 Passing Async data through a segue sender for Firebase
It turns out I was correct in my assumption the the chain of events was off. Following the model suggested by @achrefGassoumi, I moved the datatask to a Singleton Service here:
import Foundation
struct CallWebService {
static let sharedInstance = CallWebService()
func logInToCaduceus(u: String, p: String, completion: @escaping (_ sKey: String) -> ()) {
let params = ["sUser": u, "sPass": p]
let url = URL(string: "https://telemed.caduceususa.com/ws/telemed.asmx/telemedLogin")!
let session = URLSession.shared
var request = URLRequest(url: url)
request.httpMethod = "POST"
do {
request.httpBody = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
} catch let error {
print(error.localizedDescription)
}
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
let task = session.dataTask(with: request as URLRequest, completionHandler: { data, response, error in
guard error == nil else { return }
guard let data = data else { return }
do {
if let parsedJSON = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any] {
let parsedData = parsedJSON["d"] as! [String:Any]
let key = parsedData["key"] as! String
DispatchQueue.main.async {
completion(key)
}
}
} catch let error {
print(error.localizedDescription)
}
})
task.resume()
}
}
Then my two controllers look like this:
LoginVC
import UIKit
class LoginVC: UIViewController {
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if (segue.destination.isKind(of: FirebaseVC.self)) {
let vc = segue.destination as! FirebaseVC
if let sKey = sender as? String {
vc.sessionKey = sKey
}
}
}
@IBOutlet weak var username: UITextField!
@IBOutlet weak var password: UITextField!
@IBOutlet weak var validationBox: UITextView!
@IBAction func logInAction(_ sender: UIButton) {
guard let user = username.text, !user.isEmpty else {
validationBox.text = "Please enter valid credentials"
return
}
guard let pass = password.text, !pass.isEmpty else {
validationBox.text = "Please enter valid credentials"
return
}
CallWebService.sharedInstance.logInToCaduceus(u: username.text!, p: password.text!, completion: {(sessionKey: String) -> Void in
print(sessionKey)
self.performSegue(withIdentifier: "FirebaseVC", sender: sessionKey)
}
)
}
override func viewDidLoad() {
super.viewDidLoad()
//validationBox.textAlignment = .center
validationBox.text = "Ready..."
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
AND THE receiving FirebaseVC
import UIKit
class FirebaseVC: UIViewController {
private var _sessionKey = String()
var sessionKey : String {
get { return _sessionKey }
set { _sessionKey = newValue }
}
@IBOutlet weak var sessionKeyTestBox: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
sessionKeyTestBox.text = _sessionKey
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
Excuse my (non-swift) Javascript terminology but essentially I moved the data call into a service and then place a callback method in the service with the completion method to ensure the the performSegue doesn't fire until the data has been received and parsed out. So when i submit the log in form data to the server the segue doesn't fire until that async call has been completed.
When Firebase query removes Observer with handle, other observers are not called
It was my mistake in the code, I changed FriendsObserver and now everything works.
class FriendsObserver: FirebaseObserver {
static let shared = FriendsObserver()
private override init() {
super.init()
}
// MARK: - Queues
private let queue = DispatchQueue(label: "com.myapp.FriendsObserver.queue")
// MARK: - Data
private var userFriendshipStartObservers = [String : DatabaseHandle]()
private var userFriendshipEndObservers = [String : DatabaseHandle]()
open func observeSpecificUserFriendshipStart(_ observer: FirebaseObserverDelegate, isObserve: Bool, userID: String, startHandler: ((_ friend: FriendModel) -> Void)?, fail: ((_ error: Error) -> Void)?) {
let observerID = observer.observerID
let realmManager = RealmManager()
let timestamp = Date().currentTimestamp
guard let currentUserID = realmManager.getCurrentUser()?.id else { return }
let query = Database.database().reference().child(FriendsPaths.MainGateways.friends.description).child(currentUserID).child(FriendsPaths.SubGateways.userFriends.description).queryOrdered(byChild: "friendID").queryEqual(toValue: userID)
if !isObserve {
guard let handle = userFriendshipStartObservers[observerID] else { return }
query.removeObserver(withHandle: handle)
userFriendshipStartObservers[observerID] = nil
// system
removeObserverModel(observerID, handle: handle)
return
}
DispatchQueue.global(qos: .background).async {
var isContinue = true
self.queue.sync {
if self.userFriendshipStartObservers[observerID] != nil {
isContinue = false
}
}
guard isContinue else { return }
var handle: DatabaseHandle = 0
handle = query.observe(.childAdded, with: { (snapshot) in
guard snapshot.exists() else { return }
guard let dict = snapshot.value as? [String : Any] else { return }
guard let friendModel = Mapper<FriendModel>().map(JSON: dict) else { return }
guard timestamp < friendModel.friendshipTimeStamp else { return }
if friendModel.friendID == userID {
startHandler?(friendModel)
}
}, withCancel: { (error) in
fail?(error)
})
self.queue.sync {
self.userFriendshipStartObservers[observerID] = handle
self.addObserver(observerID, handle: handle, query: query, ref: nil)
}
}
}
/// Only one observer on one object
open func observeUserFriendshipEnd(_ observer: FirebaseObserverDelegate, isObserve: Bool, friendID: String, endHandler: ((_ removedFriendModel: FriendModel) -> Void)?, fail: ((_ error: Error) -> Void)?) {
let observerID = observer.observerID
let realmManager = RealmManager()
guard let currentUserID = realmManager.getCurrentUser()?.id else { return }
let query = Database.database().reference().child(FriendsPaths.MainGateways.friends.description).child(currentUserID).child(FriendsPaths.SubGateways.userFriends.description).queryOrdered(byChild: "friendID").queryEqual(toValue: friendID)
if !isObserve {
guard let handle = userFriendshipEndObservers[observerID] else { return }
query.removeObserver(withHandle: handle)
userFriendshipEndObservers[observerID] = nil
// system
removeObserverModel(observerID, handle: handle)
return
}
DispatchQueue.global(qos: .background).async {
var isContinue = true
self.queue.sync {
if self.userFriendshipEndObservers[observerID] != nil {
isContinue = false
}
}
guard isContinue else { return }
var handle: DatabaseHandle = 0
handle = query.observe(.childRemoved, with: { (snap) in
guard snap.exists() else { return }
guard let dict = snap.value as? [String : Any] else { return }
guard let removedFriendModel = Mapper<FriendModel>().map(JSON: dict) else { return }
let friendRealmManager = FriendRealmManager()
friendRealmManager.removeFriend(removedFriendModel.friendID)
if removedFriendModel.friendID == friendID {
endHandler?(removedFriendModel)
}
}, withCancel: { (error) in
fail?(error)
})
self.queue.sync {
self.userFriendshipEndObservers[observerID] = handle
self.addObserver(observerID, handle: handle, query: query, ref: nil)
}
}
}
}
Best practices for handling changes to the UINavigationItem of child view controllers in a container controller?
So the solution that I have currently implemented is to create a category on UIViewController with methods that allow you to set the right bar buttons of that controller's navigation item and then that controller posts a notification letting anyone who cares know that the right bar button items have been changed.
In my container controller I listen for this notification from the currently selected view controller and update the container controller's navigation item accordingly.
In my scenario the container controller overrides the method in the category so that it can keep a local copy of the right bar button items that have been assigned to it and if any notifications are raised it concatenates its right bar button items with its child's and then sends up a notification just incase it is also inside a container controller.
Here is the code that I am using.
UIViewController+ContainerNavigationItem.h
#import <UIKit/UIKit.h>
extern NSString *const UIViewControllerRightBarButtonItemsChangedNotification;
@interface UIViewController (ContainerNavigationItem)
- (void)setRightBarButtonItems:(NSArray *)rightBarButtonItems;
- (void)setRightBarButtonItem:(UIBarButtonItem *)rightBarButtonItem;
@end
UIViewController+ContainerNavigationItem.m
#import "UIViewController+ContainerNavigationItem.h"
NSString *const UIViewControllerRightBarButtonItemsChangedNotification = @"UIViewControllerRightBarButtonItemsChangedNotification";
@implementation UIViewController (ContainerNavigationItem)
- (void)setRightBarButtonItems:(NSArray *)rightBarButtonItems
{
[[self navigationItem] setRightBarButtonItems:rightBarButtonItems];
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter postNotificationName:UIViewControllerRightBarButtonItemsChangedNotification object:self];
}
- (void)setRightBarButtonItem:(UIBarButtonItem *)rightBarButtonItem
{
if(rightBarButtonItem != nil)
[self setRightBarButtonItems:@[ rightBarButtonItem ]];
else
[self setRightBarButtonItems:nil];
}
@end
ContainerController.m
- (void)setRightBarButtonItems:(NSArray *)rightBarButtonItems
{
_rightBarButtonItems = rightBarButtonItems;
[super setRightBarButtonItems:_rightBarButtonItems];
}
- (void)setSelectedViewController:(UIViewController *)selectedViewController
{
if(_selectedViewController != selectedViewController)
{
if(_selectedViewController != nil)
{
// Stop listening for right bar button item changed notification on the view controller.
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter removeObserver:self name:UIViewControllerRightBarButtonItemsChangedNotification object:_selectedViewController];
}
_selectedViewController = selectedViewController;
if(_selectedViewController != nil)
{
// Listen for right bar button item changed notification on the view controller.
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self selector:@selector(_childRightBarButtonItemsChanged) name:UIViewControllerRightBarButtonItemsChangedNotification object:_selectedViewController];
}
}
}
- (void)_childRightBarButtonItemsChanged
{
NSArray *childRightBarButtonItems = [[_selectedViewController navigationItem] rightBarButtonItems];
NSMutableArray *rightBarButtonItems = [NSMutableArray arrayWithArray:_rightBarButtonItems];
[rightBarButtonItems addObjectsFromArray:childRightBarButtonItems];
[super setRightBarButtonItems:rightBarButtonItems];
}
Related Topics
Dynamically Passing Closure with Keypaths to a Sorting Function
How to Convert a Persian Date into a Gregorian in Swift
Array Element Checking in Swift
How to Authorize Twitter with Swifter
Skaction Works in Didmovetoview But Doesn't Works in Function
How to Set the Local Storage Before a Uiwebview Loading Its Initial Request
Making the Map Zoom to User Location and Annotation (Swift 2)
Google Signin Cocoapods Deprecated
Create Hash in Swift Using Key and Message
Saving Array Using Nsuserdefaults Crashes App
Updating Label Takes Too Long (Swift)
Avaudioplayer Working on Simulator But Not on Real Device
How to Get the Opposite Value of a Bool in Swift
Swiftui Animation and Subsequent Reverse Animation to Original State
How to Get the Index (String.Index) Value from the Cursor Position of a Uitextview Element in Swift