viewDidLoad, is it called only once? - ios

I am a beginner in iOS development, and I am following one tutorial using firebase database to make a simple chat app. Actually I am confused with the use of viewDidLoad method.
Here is the screenshot of the app: https://ibb.co/gqD4Tw
I don't understand why retrieveMessage() method is put on viewDidLoad when I want to send data (chat message) to firebase database, I used sendButtonPressed() method (which is an IBAction) and when I want to retrieve data from the database, I use retrieveMessage().
The retrieveMessage() method is called on viewDidLoad, as far as I know the viewDidLoad method is called only once after the view is loaded into memory. We usually use it for initial setup.
So, if viewDidLoad is called only once in initial setup, why the retrieveMessage() method can retrieve all the message that I have sent to my own database over and over again, after I send message data to the database ?
I don't understand why retrieveMessage() method is put on viewDidLoad below is the simplified code:
class ChatViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
var messageArray = [Message]()
#IBOutlet var messageTextfield: UITextField!
#IBOutlet var messageTableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
//Set as the delegate and datasource :
messageTableView.delegate = self
messageTableView.dataSource = self
//the delegate of the text field:
messageTextfield.delegate = self
retrieveMessage()
///////////////////////////////////////////
//MARK: - Send & Recieve from Firebase
#IBAction func sendPressed(_ sender: AnyObject) {
// Send the message to Firebase and save it in our database
let messageDB = FIRDatabase.database().reference().child("message")
let messageDictionary = ["MessageBody":messageTextfield.text!, "Sender": FIRAuth.auth()?.currentUser?.email]
messageDB.childByAutoId().setValue(messageDictionary) {
(error,ref) in
if error != nil {
print(error!)
} else {
self.messageTextfield.isEnabled = true
self.sendButton.isEnabled = true
self.messageTextfield.text = ""
}
}
}
//Create the retrieveMessages method :
func retrieveMessage () {
let messageDB = FIRDatabase.database().reference().child("message")
messageDB.observe(.childAdded, with: { (snapshot) in
let snapshotValue = snapshot.value as! [String:String]
let text = snapshotValue["MessageBody"]!
let sender = snapshotValue["Sender"]!
let message = Message()
message.messsageBody = text
message.sender = sender
self.messageArray.append(message)
self.messageTableView.reloadData()
})
}
}

viewDidLoad method is called only once in ViewController lifecycle.
The reason retrieveMessage() is called in viewDidLoad because it's adding observer to start listening for received and sent message. Once you receive or send message then this block(observer) is called and
then adding that text in array self.messageArray.append(message) and updating tableview.

viewDidLoad gets called only once but the firebase functions starts a listener, working in background and syncronizeing data.
Its called in viewDidLoad because it tells -> When this view loads, start listening for messages.

ViewDidLoad() is only called upon initializing the ViewController.
If you want to have a function called every time the user looks at the VC again (e.g. after a segue back from another VC) you can just use ViewDidAppear().
It is also called when ViewDidLoad() is called.

Related

Update tableView row from AppDelegate Swift 4

[![enter image description here][1]][1]
Hello. I have a tableview like in the picture above and I'm receiving some silent push notifications. Depending on them I need to reload a specific cell from the tableView. Since I'm getting the notification in the AppDelegate and there at the moment I'm reloading the whole tableView...but personally I don't find this the best solution since I only need to update a specific row.
Any hints please how can I update just a specific cell from appDelegate?
if userInfo["notification_type"] as? String == "update_conversation" {
if let rootVC = (self.window?.rootViewController as? UINavigationController)?.visibleViewController {
if rootVC is VoiceViewController {
let chatRoom = rootVC as! VoiceViewController
chatRoom.getConversations()
// the get Conversations method makes a call to api to get some data then I reload the whole tableView
}
}
func getConversations() {
let reachabilityManager = NetworkReachabilityManager()
if (reachabilityManager?.isReachable)! {
ServerConnection.getAllConversation { (data) in
if let _ = data{
self.conversations = data
self.onlineRecent = self.conversations
GlobalMainQueue.async {
self.mainTableView.reloadData()
}
}
}
}
}
This is my getConversation method which is used in VoiceViewController to populate my tableview
Have the app delegate broadcast an app-specific notification center notification (on the main thread). Have the view controller that contains your table view listen for that notification and update the cell in question as needed. That way you don't contaminate your app delegate. The app delegate should only deal with system level app stuff, not business logic.
You could get your row’s cell using self.mainTableView.cellForRow(at:IndexPath(…), and update it directly.
Or, I’ve found you save a load of time and your view controllers end up more reliable using ALTableViewHelper [commercial - available on Framework Central here]. It’s free to try.
The helper does the most of the work - you describe how the data connects to the UITableView. I’ve put together an example (on GitHub here), which I hope is something like what you’re trying to do.
import ALTableViewHelper
class VoiceViewController {
// #objc for the helper to see the var’s, and dynamic so it sees any changes to them
#obj dynamic var conversations: Any?
#obj dynamic var onlineRequest: Any?
func viewDidLoad() {
super.viewDidLoad()
tableView.setHelperString(“””
section
headertext "Conversation Status"
body
Conversation
$.viewWithTag(1).text <~ conversations[conversations.count-1]["title"]
$.viewWithTag(2).text <~ "At \\(dateFormat.stringFromDate(conversations[conversations.count-1]["update"]))"
UpdateButton
$.viewWithTag(1).isAnimating <~ FakeConversationGetter.singleton.busy
“””, context:self)
}
func getConversations() {
let reachabilityManager = NetworkReachabilityManager()
if (reachabilityManager?.isReachable)! {
ServerConnection.getAllConversation { (data) in
if let _ = data {
// change the data on the main thread as this causes the UI changes
GlobalMainQueue.async {
self.conversations = data
self.onlineRequest = self.conversations
}
}
}
}
}

Set UILabel Text at Completion of CloudKit Asyncronous Operation in Swift

I'm new to Swift, and asynchronous code in general, so tell me if this is way off. Basically I want to:
Open the App
That triggers a read of CloudKit records
Once the read is complete a UILabel will display the number of records retrieved
This clearly isn't useful in itself, but as a principle it will help me to understand the asynchronous code operation, and how to trigger actions on their completion.
// In ViewController Swift file:
class ViewController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
readDatabase()
}
#IBOutlet weak var myLabel: UILabel!
}
let VC=ViewController()
//In Another Swift file:
func readDatabase() {
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "myRecord", predicate: predicate)
let container = CKContainer.default()
let privateDB = container.privateCloudDatabase
privateDB.perform(query, inZoneWith:nil) { (allRecs, err) in
VC.myLabel.text = ("\(allRecs?.count) records retreived")
/*
ERROR OCCURS IN LINE ABOVE:
CONSOLE: fatal error: unexpectedly found nil while unwrapping an Optional value
BY CODE LINE: Thread 8:EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
*/
}
}
I'm able to set the text field from within the viewDidLoad function, so why not from a function called within that function?
A few other things I've tried:
Use async dispatch to put it on thread 1
Implement a var with within the ViewController class, with a didSet that sets the text, set the var to the desired value in the privateDB.perform code to trigger the change
These both create the same problem as above.
Yes, I know there isn't any error handling in .perform, and yes there are records. If I trigger the setting of the UILabel text to the record count manually a few seconds after the view has loaded, it works fine.
So the question is...
How do I use the completion of the database read as a trigger to load attributes of the records to the view?
Thanks
Edit
What actually happened here was that VC was created globally, but never presented - since loadView was never called for it, myLabel didn't exist and, being a force unwrapped property, caused a crash when it was referenced
The problem is with this line: let VC=ViewController(). Here you instantiate a new instance of your ViewController class and try to set the label on that newly created instance. However, you would want to set the label on your viewController instance that is currently displayed.
Just change this line VC.myLabel.text = ("\(allRecs?.count) records retreived") to self.myLabel.text = ("\(allRecs?.count) records retreived") and it should work fine.
Got it, the solution looks like this:
// In ViewController Swift file:
typealias CompletionHandler = (_ recCount:Int,_ err:Error?) -> Void
class ViewController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
readDatabase(completionHandler: { (recCount,success) -> Void in
if err == nil {
self.myLabel.text = "\(recCount) records loaded"
} else {
self.myLabel.text = "load failed: \(err)"
}
})
}
#IBOutlet weak var myLabel: UILabel!
}
//In Another Swift file:
func readDatabase() {
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "myRecord", predicate: predicate)
let container = CKContainer.default()
let privateDB = container.privateCloudDatabase
privateDB.perform(query, inZoneWith:nil) { (allRecs, err) in
if let recCount = allRecs?.count {
completionHandler(recCount,err)
} else {
completionHandler(0,err)
}
}
}
The difference between this and the original is that this uses the CompletionHandler typealias in the function call for loading the database records, which returns the count of records and an optional error.
The completion operation can now live in the ViewController class and access the UILabel using self.myLabel, which solves the error that was occurring earlier, while keeping the database loading code separate to the ViewController class.
This version of the code also has basic error handling.

Why is selector being sent to previous view controller?

I have an app with a data model class that declares a protocol, and two view controllers embedded in a navigation controller. The data model class is a shared instance. Both view controllers are delegates of the data model. The second view controller has a UITableView.
On start, calls to data model functions from the first view controller work as expected. When I segue from the first view controller to the second, calls to data model functions work as expected as well.
However, when I navigate back to the first view controller and a data model function is called, the app crashes with this error:
2017-04-03 09:48:12.623027 CoreDataTest[3207:1368182]
-[CoreDataTest.PracticesViewController noDupeWithResult:]: unrecognized selector sent to instance 0x15fe136e0
That PracticesViewController is the second view controller. I don't understand why a selector is being sent to what I am thinking of as the previous view controller. My expectation is that the selector should be sent to the first view controller that has just been navigated back to,
I am self-taught, so I presume there is something basic that I am missing, but I don't know what I don't know. Can someone explain why the crash is happening?
Code for the data model
import Foundation
import CoreData
import UIKit
#objc protocol PracticesDataDelegate {
#objc optional func practicesLoadError(headline:String,message:String)
#objc optional func practicesLoaded(practices:[NSManagedObject])
#objc optional func practiceStored()
#objc optional func practiceDeleted()
#objc optional func noDupe(result:String)
}
class PracticesDataModel {
static let sharedInstance = PracticesDataModel()
private init () {}
var delegate: PracticesDataDelegate?
var practices: [NSManagedObject] = []
// some code omitted . . .
/// Check for a duplicate exercise
func checkForDupe(title:String,ngroup:String,bowing:String,key:String){
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext = appDelegate.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Practice")
let predicate = NSPredicate(format: "exTitle == %# AND notegroup == %# AND bowing == %# AND key == %#", title, ngroup, bowing, key)
fetchRequest.predicate = predicate
do {
practices = try managedContext.fetch(fetchRequest)
if practices.count == 0 {
self.delegate?.noDupe!(result:"none") // exception happens here
} else {
self.delegate?.noDupe!(result:"dupe")
}
} catch let error as NSError {
// to come
}
}
The top of the first view controller
import UIKit
class galamianSelection: UIViewController, ExcerciseDataModelDelegate, PracticesDataDelegate {
let exerciseModel = ExerciseDataModel.sharedInstance
let pModel = PracticesDataModel.sharedInstance
// some code omitted . . .
//// THE VIEW ////
override func viewDidLoad() {
super.viewDidLoad()
exerciseModel.delegate = self
pModel.delegate = self
exerciseModel.loadExercises()
}
//// RESPOND TO PRACTICE DATA MODEL ////
func practiceStored() {
print("exercise stored")
}
func noDupe(result: String) {
if result == "none" {
let d = Date()
pModel.storePractice(date: d, exTitle: theExerciseTitle.text!, notegroup: theNoteGroup.text!, bowing: theBowings.text!, rythem: theRhythms.text!, key: theKey.text!, notes: "")
} else {
print("dupe")
}
}
The top of the second view controller
import UIKit
class PracticesViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, PracticesDataDelegate {
#IBOutlet weak var tableView: UITableView!
let pModel = PracticesDataModel.sharedInstance
// some code omitted . . .
//// THE VIEW ////
override func viewDidLoad() {
super.viewDidLoad()
pModel.delegate = self
pModel.getPractices()
}
delegate is properly set to self in both view controllers.
I am happy to provide more code is needed, but I suspect someone who knows can diagnose from what I've provided.
There are a few issues here.
You are trying to reuse a delegate but only setting it in viewDidLoad. viewDidLoad is only called when, as the name implies, the view initially loads. That means that if you have VC1 in a navigation controller, then you push to VC2, then pop back to VC1, viewDidLoad will not be called a second time. The view has already loaded. If you want a piece of code to be called every time a view controller comes back into focus, you should put it into viewWillAppear: or viewDidAppear:
You are force unwrapping an optional protocol method which you haven't actually implemented (a bug which you're seeing as a result of the first issue, but it's still an issue on its own). Change the force unwrap to an optional self.delegate?.noDupe?(result:"none")
You also declared your delegate like this var delegate: PracticesDataDelegate?. This makes your class retain its delegate and is generally not the right behavior. In your case it actually causes a retain cycle (until you change the delegate). You should change this declaration to weak var delegate: PracticesDataDelegate?
What appears to be happening is:
In PracticesViewController you set the model's delegate to self, as in pModel.delegate = self.
You navigate back to your first view controller. This means that PracticesViewController gets deallocated. But the model's delegate has not been changed, so it's still pointing to the memory location where the view controller used to be.
Later (but soon), your model tries to call a method on its delegate, but it can't because it was deallocated. This crashes your app.
You could reassign the value of the delegate, for example in viewDidAppear. Or you could have your model check whether its delegate implements a method before attempting to call it. That's a standard practice for optional protocol methods-- since they don't have to be implemented, you check first before calling them.
In general, don't use ! in Swift unless you want your code to crash there if something goes wrong.

Swift iOS -Which viewController lifecycle event to use to send data to Firebase after a view changes

I have some information to send to Firebase. The thing is I want to send the data but I also have to pull the data from there first. The data I get is based on the users input.
I'm already making several nested async calls to Firebase. Not only do i have to wait for the calls to finish to make sure the data has been set but I don't want to have the user waiting around unnecessarily when they can leave the scene and the data can be pulled and changed in a background task.
I was thinking about using a NSNotification after the performSegueWithIdentifier is triggered. The observer for the notification would be inside viewWillDisappear.
Is this safe to do and if not what's the best way to go about it?
Code:
var ref: FIRDatabaseReference!
let uid = FIRAuth.auth()?.currentUser?.uid
let activityIndicator = UIActivityIndicatorView()
override func viewDidLoad() {
super.viewDidLoad()
self.ref = FIRDatabase.database().reference().child(self.uid!)
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(fetchSomeValueFromFBThenUpdateAndResendAnotherValue), name: "FbFetchAndSend", object: nil)
}
#IBAction func buttonPressed(sender: UIButton) {
activityIndicator.startAnimating()
levelTwoRef //send levelTwo data to FB run 1st callback
scoreRef //send score data to FB run 2nd callback
powerRef //send power data to FB run 3rd callback
lifeRef //send life data to FB run Last callback for dispatch_async...
dispatch_async(dispatch_get_main_queue()){
activityIndicator.stopAnimating()
performSegueWithIdentifier....
//Notifier fires after performSegue???
NSNotificationCenter.defaultCenter().postNotificationName("FbFetchAndSend", object: nil)
}
}
func fetchSomeValueFromFBThenUpdateAndResendAnotherValue(){
let paymentRef = ref.child("paymentNode")
paymentRef?.observeSingleEventOfType(.Value, withBlock: {
(snapshot) in
if snapshot.exists(){
if let dict = snapshot.value as? [String:AnyObject]{
let paymentAmount = dict["paymentAmount"] as? String
let updatePayment = [String:AnyObject]()
updatePayment.updateValue(paymentAmount, forKey: "paymentMade")
let updateRef = self.ref.child("updatedNode")
updateRef?.updateChildValues(updatePayments)
}
You are adding the observer in viewWillDisappear, So it won't get fired because it won't be present when your segue is performed.
Add the observer in viewDidLoad and it will work.
But if you just want to call fetchSomeValueFromFBThenUpdateAndResendAnotherValue() when the view is disappearing then there is no need for observer.
Simply call the method on viewWillDisappear like this -
override func viewWillDisappear(animated: Bool)
{
super.viewWillDisappear(animated)
fetchSomeValueFromFBThenUpdateAndResendAnotherValue()
}

Firebase removeAllObservers() keeps making calls when switching views -Swift2 iOS

I read other stack overflow q&a's about this problem but it seems to be a tabBarController issue which I haven't found anything on.
I have a tabBarController with 3 tabs. tab1 is where I successfully send the info to Firebase. In tab2 I have tableView which successfully reads the data from tab1.
In tab2 my listener is in viewWillAppear. I remove the listener in viewDidDisappear (I also tried viewWillDisappear) and somewhere in here is where the problem is occurring.
override func viewDidDisappear(animated: Bool) {
self.sneakersRef!.removeAllObservers()
//When I tried using a handle in viewWillAppear and then using the handle to remove the observer it didn't work here either
//sneakersRef!.removeObserverWithHandle(self.handle)
}
Once I switch from tab2 to any other tab, the second I go back to tab2 the table data doubles, then if I switch tabs again and come back it triples etc. I tried setting the listener inside viewDidLoad which prevents the table data from duplicating when I switch tabs but when I send new info from tab1 the information never gets updated in tab2.
According to the Firebase docs and pretty much everything else I read on stackoverflow/google the listener should be set in viewWillAppear and removed in viewDidDisappear.
Any ideas on how to prevent my data from duplicating whenever I switch between back forth between tabs?
tab1
import UIKit
import Firebase
import FirebaseAuth
import FirebaseDatabase
class TabOneController: UIViewController{
var ref:FIRDatabaseReference!
#IBOutlet weak var sneakerNameTextField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
self.ref = FIRDatabase.database().reference()
}
#IBAction func sendButton(sender: UIButton) {
let dict = ["sneakerName":self.sneakerNameTextField.text!]
let usersRef = self.ref.child("users")
let sneakersRef = usersRef.child("sneakers").childByAutoID()
sneakersRef?.updateChildValues(dict, withCompletionBlock: {(error, user) in
if error != nil{
print("\n\(error?.localizedDescription)")
}
})
}
}
tab2
import UIKit
import Firebase
import FirebaseAuth
import FirebaseDatabase
class TabTwoController: UITableViewController{
#IBOutlet weak var tableView: UITableView!
var handle: Uint!//If you read the comments I tried using this but nahhh didn't help
var ref: FIRDatabaseReference!
var sneakersRef: FIRDatabaseReference?
var sneakerArray = [String]()
override func viewDidLoad() {
super.viewDidLoad()
self.ref = FIRDatabase.database().reference()
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
let usersRef = self.ref.child("users")
//Listener
self.sneakersRef = usersRef.child("sneakers")
//I tried using the handle
//---self.handle = self.sneakersRef!.observeEventType(.ChildAdded...
self.sneakersRef!.observeEventType(.ChildAdded, withBlock: {(snapshot) in
if let dict = snapshot.value as? [String:AnyObject]{
let sneakerName = dict["sneakerName"] as? String
self.sneakerArray.append(sneakerName)
dispatch_async(dispatch_get_main_queue(), {
self.tableView.reloadData()
})
}
}), withCancelBlock: nil)
}
//I also tried viewWillDisappear
override func viewDidDisappear(animated: Bool) {
self.sneakersRef!.removeAllObservers()
//When I tried using the handle to remove the observer it didn't work either
//---sneakersRef!.removeObserverWithHandle(self.handle)
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.sneakerArray.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCellWithIdentifier("sneakerCell", forIndexPath: indexPath)
cell.textLabel?.text = sneakerArray[indexPath.row]
return cell
}
}
tab3 does nothing. I just use it as an alternate tab to switch back and forth with
The removeAllObservers method is working correctly. The problem you're currently experiencing is caused by a minor missing detail: you never clear the contents of sneakerArray.
Note: there's no need for dispatch_async inside the event callback. The Firebase SDK calls this function on the main queue.
The official accepted Answer was posted by #vzsg. I'm just detailing it for the next person who runs into this issue. Up vote his answer.
What basically happens is when you use .childAdded, Firebase loops around and grabs each child from the node (in my case the node is "sneakersRef"). Firebase grabs the data, adds it to the array (in my case self.sneakerArray), then goes goes back to get the next set of data, adds it to the array etc. By clearing the array on the start of the next loop, the array will be empty once FB is done. If you switch scenes, you'll always have an empty array and the only data the ui will display is FB looping the node all over again.
Also I got rid of the dispatch_async call because he said FB calls the function in the main queue. I only used self.tableView.reloadData()
On a side note you should subscribe to firebase-community.slack.com. That's the official Firebase slack channel and that's where I posted this question and vzsg answered it.
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
//CLEAR THE ARRAY DATA HERE before you make your Firebase query
self.sneakerArray.removeAll()
let usersRef = self.ref.child("users")
//Listener
self.sneakersRef = usersRef.child("sneakers")
//I tried using the handle
//---self.handle = self.sneakersRef!.observeEventType(.ChildAdded...
self.sneakersRef!.observeEventType(.ChildAdded, withBlock: {(snapshot) in
if let dict = snapshot.value as? [String:AnyObject]{
let sneakerName = dict["sneakerName"] as? String
self.sneakerArray.append(sneakerName)
//I didn't have to use the dispatch_async here
self.tableView.reloadData()
}
}), withCancelBlock: nil)
}
Thank for asking this question.
I have also faced same issue in past. I have solved this issue as follow.
1) Define array which contain list of observer.
var aryObserver = Array<Firebase>()
2) Add observer as follow.
self.sneakersRef = usersRef.child("sneakers")
aryObserver.append( self.sneakersRef)
3) Remove observer as follow.
for fireBaseRef in self.aryObserver{
fireBaseRef.removeAllObservers()
}
self.aryObserver.removeAll() //Remove all object from array.

Resources