Losing Data when switching to another view controller Xcode Swift - swift

I'm having trouble figuring out exactly what's going on. Basically, I'm opening my first view controller which takes a users info from firebase. i.e. the name and uid. I'm sending data by didSet() in my second view controller (this is sent before the second view controller is opened). The data does get sent because I've tested it by using print(). However, when I navigate to my second view controller (using a tab controller and navigation controller, the data is no longer there when I navigate to my second view controller.
import Foundation
struct User {
let uid: String
let name: String
init(uid: String, dictionary: [String: Any]) {
self.uid = uid
self.name = dictionary["uname"] as? String ?? ""
}
}
Then in my first controller I use:
guard let uid = Auth.auth().currentUser?.uid else {
print("Could not init User at start")
return }
Database.database().fetchUser(withUID: uid) { (user) in
secondController.user = user
}
..
In my secondView controller I have:
var user: User? {
didSet {
printUser()
}
}
printUser(){
let uid = user?.uid
print(uid)
}
This is how I know the data is sent.
However, when I Navigate to the second viewcontroller by a tab bar controller and navigation controller, the data is not there. i.e I have printUser() in view did load, but it comes up as an error because user?.uid is nil.
I'm navigating viewControllers through storyboard.

Related

Swift 4 Passing data to another controller Whilst staying in the same controller without presenting

I have a product viewcontroller contains the following:
class arProduct: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {
// cart ready array
var cartArray = [String]()
var cartIcon: UIImage?
}
In which the user using the interface will finalize the product details and clicks "add to cart button"
I have another view controller called cart contains the following:
class arCart: UIViewController {
//var
var cartMasterArray = [[String]]()
var cartImageArray = [UIImage]()
}
Usually what I'd do is do a prepare in product, setting arCart as a destination and assign variables. Once the performsegue is called everything works fine. However what I need here is something like this:
prepare (...) {
let destination = segue.destination as arCart!
destination.cartMasterArray.append(self.cartArray)
}
which works fine but that means I have to segue for this to work.
BUT, the user might not want to segue after add to cart is clicked, he might want to go to another product and append another product to the cart, only when he is finished he might go to cart, and he might not go to cart from product view.
So my question is, is there a way to pass data to the cart vc without having to segue to it? i.e a prepare that will set a value globally that can be used without a segue? how to store a variable in memory basically that is independent of controller cycle.
Thanks,
Edited as requested with code snippets.
There may be a better way to do this. Saving your cart data to UserDefaults would be more stable than passing it between view controllers. Then simply read from UserDefaults when the cart view controller is loaded.
Here is an example:
let cartArray =["item1", "item2"]
let defaults = UserDefaults.standard
defaults.set(cartArray, forKey: "cartArray")
Then when you need to load the cart data:
let savedCart = defaults.object(forKey: "cartArray") as? [String] ?? [String]()
Create a model to hold your products, then a (singleton) data controller to handle the cart.
class Product {
var name: String = ""
var image: UIImage?
// Any other attributes of the product
// Add init(s) and method as appropriate to set-up products
}
struct Products {
var products: [Product] = []
func loadProducts() {
// Load your list of products from somewhere
}
}
struct Cart {
private var itemsInCart: [Product] = []
func addProductToCart(_ product: Product) {
itemsInCart.append(product)
}
func productsInCart() -> [Product] {
return itemsInCart
}
// Other methods to handle the cart
}
// Better for the cart view controller to access the data controller,
// but you can set the cart data in the segue...
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let vc = segue.destination as? MyViewControllerClass {
vc.cart = Cart.productsInCart()
}
}
You can then access the products and cart data from anywhere in your app. Obviously you'll need to populate the products in some way, presumably from a server or a database.
You could also use delegates to communicate between view controllers.

JSQMessagesviewcontroller can't perform segues

so I created a chat view controller using the JSQMessagesViewController following this tutorial here: https://learnappmaking.com/chat-app-ios-firebase-swift-xcode/#comment-1930 my code is more or less the same, I didn't tweak anything significant in it, the tutorial is only for a single view controller so I added another view controllers for the app but every time it perform segues, I get the error SIGABRT, no matter if I segues with performSegue or with the back button in navigation bar, it keeps giving signal SIGABRT. any help would be appreciated.
this is my viewdidload:
override func viewDidLoad() {
super.viewDidLoad()
senderId = "1111"
senderDisplayName = "Bob"
title = "Steve"
inputToolbar.contentView.leftBarButtonItem = nil
collectionView.collectionViewLayout.incomingAvatarViewSize = CGSize.zero
collectionView.collectionViewLayout.outgoingAvatarViewSize = CGSize.zero
let query = Constants.refs.databaseChats.queryLimited(toLast: 10)
_ = query.observe(.childAdded, with: { [weak self] snapshot in
if let data = snapshot.value as? [String: String],
let id = data["sender_id"],
let name = data["name"],
let text = data["text"],
!text.isEmpty
{
if let message = JSQMessage(senderId: id, displayName: name, text: text)
{
self?.messages.append(message)
self?.finishReceivingMessage()
}
}
})
// Do any additional setup after loading the view.
}
SIGABRT (signal abort) is typically from a referencing error in your storyboard. Did you ever change the name of a class or make a connection from a button of one view controller to another and then delete it? If you changed the name of a class you must make the sure the name in the code of the class matches that. If you deleted a button connection between view controllers, click on the controller itself and under the connections tab you must delete it.

Handle deep link notification

I'm adding deep linking to my app and getting stuck at handing off the URL to the view controller. I try to use notification but it doesn't work all the times.
My App Delegate:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
let notification = Notification(name: "DeepLink", object: url)
NotificationCenter.default.post(notification)
return true
}
}
And View Controller:
class ViewController: UIViewController {
func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.handleDeepLink), name: "DeepLink", object: nil)
}
#objc private func handleDeepLink(notification: Notification) {
let url = notification.object as! URL
print(url)
}
}
Problem: when my app is cold launched the notification is not handled, which I guess is due to the View Controller not having time to register itself as the observer yet. If I press the Home button, go to Notes and click on the deep link a second time, everything works as it should.
Question: how can I register the View Controller to observe the notification when the app is cold launched? Or is there a better way to send the URL from the App Delegate to the View Controller?
I think an initial assumption is to assume your view controller does not have time to register itself which means that the connection between URL and View controller must be decoupled and reside outside of the view controller.
I would then use some kind of lookup to instantiate the view controller when a URL is received.
For example,
if url.contains(“x”) {
let vc = ViewController(...)
cv.url = URL // Pass contextual data to the view controller.
// Present or push cv
}
As your app gets more complex you have to manage more challenging scenarios, like standing-up entire controller stacks, or removing presented controllers.
I followed the example in the LaunchMe app. Here's what solved my problem:
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
// My root view controller is a UINavigationController
// Cast this to whatever class your root view controller is
guard let navigationController = self.window?.rootViewController as? UINavigationController else {
return false
}
// Navigate to the view controller that handles the URL within the navigation stack
navigationController.popToRootViewController(animated: false)
// Handle the URL
let vc = navigationController.topViewController as! ViewController
vc.handleDeepLink(url: url)
return true
}
Store if notification is not handled and load it when viewDidLoad.
var pendingNotificationInfo: PushNotificationInfo?

Trying to pass information between controllers fails, yet works from AppDelegate to controller

I have two UIViewControllers, vc1, and vc2. vc1 is embedded in a UIViewController which is embedded in a UITabBarController, but vc2 is not embedded in either.
How do I pass information from vc2 to vc1? After a user performs an action the data is saved and vc2 simply closes, so there isn't a segue to pass information. Obviously I can't reference vc1 through the Navigation stack or the TabController.
I could save to the AppDelegate, but I've read this isn't a good practice.
This is the code I use to pass information from AppDelegate to vc1 I tried it in vc2, but obviously it failed.:
let tabBarController = window!.rootViewController as! UITabBarController
if let tabBarViewControllers = tabBarController.viewControllers {
let navPostViewController = tabBarViewControllers[0] as! UINavigationController
let user = User(context: managedObjectContext)
if user.userID != nil {
print("User is loggedIn")
isUserLoggedIn = true
} else {
print("User is not loggedIn")
isUserLoggedIn = false
}
let postViewController = navPostViewController.topViewController as! PostViewController
postViewController.managedObjectContext = managedObjectContext
}
First off, I've never got into the habit of using segue to pass information. What i would recommend is that you implement the delegate pattern whenever you need to pass data between two objects. Its a lot cleaner.
For instance lets say you wanted to pass data between LoginViewController and PostViewController:
protocol LoginViewControllerDelegate:NSObjectProtocol{
func userDidLogin(data:String)
}
class LoginViewController:UIViewController {
weak var delegate:LoginViewControllerDelegate?
...
#IBAction func loginButtonPressed(sender:UIButton) {
//Perform login logic here
//If successful, tell the other controller or the 'delegate'
self.delegate?.userDidLogin(data:"Some data....")
}
}
class PostViewController:UIViewController, LoginViewControllerDelegate {
func userDidLogin(data:String) {
print("Got data from login controller: \(data)")
}
}
//How you might use this
loginViewController.delegate = postViewController
One caveat to remember is to never try to have strong references between two objects i.e. do not have the objects hold onto each other or this will cause a memory leak.

Segue async Firebase data through NavigationController (Swift)

I have been chasing this for two days but yet I am still not sure why my variable isn't being passed in my segue from my login view controller to the chat view controller via the navigation view controller.
I have a button that queries Firebase, checks if the user exists and returns a Firebase query reference for the user. I then want to pass this Firebase query reference when it finishes to the navigation controller's top view controller for use.
Inside my IBAction login button, I have:
var tempUserRef: FIRDatabaseReference?
channelRef.queryOrdered(byChild: "uid").queryEqual(toValue: uid).observeSingleEvent(of: .value, with: { (snapshot) in
if snapshot.exists() {
print("uid exist with \(snapshot.childrenCount) number of children")
for s in snapshot.children.allObjects as! [FIRDataSnapshot] {
tempUserRef = self.channelRef.child(s.key)
}
} else {
print("uid didn't exist")
if let name = self.nameField?.text { // 1
tempUserRef = self.channelRef.childByAutoId()
let channelItem = [
"name": name,
"uid": self.uid
]
tempUserRef?.setValue(channelItem)
}
}
self.userRef = tempUserRef
DispatchQueue.main.async {
self.performSegue(withIdentifier: "LoginToChat", sender: tempUserRef)
print("passsed \(self.userRef)")
}
})
Here is my segue:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if "LoginToChat" == segue.identifier {
if let navVc = segue.destination as? UINavigationController {
if let chatVc = navVc.topViewController as? ChatViewController {
chatVc.senderDisplayName = nameField?.text
if let userRef = sender as? FIRDatabaseReference {
chatVc.userRef = userRef
print("passsing \(self.userRef) to \(chatVc)")
}
}
}
}
super.prepare(for: segue, sender: sender)
}
The print statements all look good on my login controller but when I get to the chat view controller, the userRef is still nil. So my sense is that I have the right segue inputs and handoffs but that the async nature of the data is somehow out of step with my segue.
The chat view controller is using the JSQMessages library if that makes a difference.
Thanks for your help.
EDIT:
Based off feedback I've moved the super.prepare but userRef is still not being set consistently.
SECOND EDIT:
Following paulvs' suggestion, I removed my button segue. However, I did have to create another segue that connected view controller to view controller like this SO question.
Place the call to super.prepare at the end of the function. Otherwise, the segue is performed before you set your variables.