I am working with swift in Xcode 7. I am totally new to Swift, Xcode, and Firebase. I would like to have three UITableViewControllers in my iOS app. The first two TableView controllers will need dynamic content and the third TableView controller will need static content. I would like for the second and third TableView controllers to display data based on what is pressed on the previous TableView controller. All of the data will come from my Firebase. I have no idea where to start. Please point me in the right direction! Thank you!
This question is broad, in that it asks how to do three different tasks.
I think you'll be better off getting answers if you only ask for one thing at a time.
I can help you with populating a UITableViewController with Firebase.
class MyTableViewController: UITableViewController {
// your firebase reference as a property
var ref: Firebase!
// your data source, you can replace this with your own model if you wish
var items = [FDataSnapshot]()
override func viewDidLoad() {
super.viewDidLoad()
// initialize the ref in viewDidLoad
ref = Firebase(url: "<my-firebase-app>/items")
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
// listen for update with the .Value event
ref.observeEventType(.Value) { (snapshot: FDataSnapshot!) in
var newItems = [FDataSnapshot]()
// loop through the children and append them to the new array
for item in snapshot.children {
newItems.append(item as! FDataSnapshot)
}
// replace the old array
self.items = newItems
// reload the UITableView
self.tableView.reloadData()
}
}
}
This technique uses the .Value event, you can also use .ChildAdded, but then you have to keep track of .ChildChanged, '.ChildRemoved', and .ChildMoved, which can get pretty complicated.
The FirebaseUI library for iOS handles this pretty easily.
dataSource = FirebaseTableViewDataSource(ref: self.firebaseRef, cellReuseIdentifier: "<YOUR-REUSE-IDENTIFIER>", view: self.tableView)
dataSource.populateCellWithBlock { (cell: UITableViewCell, obj: NSObject) -> Void in
let snap = obj as! FDataSnapshot
// Populate cell as you see fit, like as below
cell.textLabel?.text = snap.key as String
}
I do it slightly different when I have a UITableViewController, especially for those that can push to another detail view / or show a modal view over the top.
Having the setObserver in ViewDidAppear works well. However, I didnt like the fact that when I looked into a cells detail view and subsequently popped that view, I was fetching from Firebase and reloading the table again, despite the possibility of no changes being made.
This way the observer is added in viewDidLoad, and is only removed when itself is popped from the Nav Controller stack. The tableview is not reloaded unnecessarily when the viewAppears.
var myRef:FIRDatabaseReference = "" // your reference
override func viewDidLoad() {
super.viewDidLoad()
setObserver()
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
// only called when popped from the Nav Controller stack
// if I push to another detail view my observer will remain active
if isBeingDismissed() || isMovingFromParentViewController() {
myRef.removeAllObservers()
}
}
func setObserver() {
myRef.observeEventType(.Value, withBlock: { snapshot in
var newThings = [MyThing]()
for s in snapshot.children {
let ss = s as! FIRDataSnapshot
let new = MyThing(snap: ss) // convert my snapshot into my type
newThings.append(new)
}
self.things = newThings.sort{ $0.name < $1.name) } // some sort
self.tableView.reloadData()
})
}
I also use .ChildChanged .ChildDeleted and .ChildAdded in UITableViews. They work really well and allow you use UITableView animations. Its a little more code, but nothing too difficult.
You can use .ChildChanged to get the initial data one item at a time, then it will monitor for changes after that.
If you want all your data at once in the initial load you will need .Value, I suggest you use observeSingleEventOfType for your first load of the tableview. Just note that if you also have .ChildAdded observer you will also get an initial set of data when that observer is added - so you need to deal with those items (i.e. don't add them to your data set) otherwise your items will appear twice on the initial load.
Related
When the callback for the TaskListDataSource gets called it reloads both the todayVC and the reviewVC because they are UITableViewControllers. However the plannerVC is not and the tableview property is an outlet.
#IBOutlet weak var tableView: UITableView!
Why is it that when the callback runs it crashes saying it is nil. If I am somehow able to scroll across in the page view however and and view the plannerVC it will never crash as the tableview has been loaded into memory. But why doesn't it do it initially?
override func viewDidLoad() {
super.viewDidLoad()
let taskListDataSource = TaskListDataSource {
self.todayVC.tableView.reloadData()
self.plannerVC.tableView.reloadData()
self.reviewVC.tableView.reloadData()
}
todayVC = storyboard!.instantiateViewController(identifier: "TodayViewController", creator: { coder in
return TodayViewController(coder: coder, taskListDataSource: taskListDataSource)
})
plannerVC = storyboard!.instantiateViewController(identifier: "PlannerViewController", creator: { coder in
return PlannerViewController(coder: coder, taskListDataSource: taskListDataSource)
})
reviewVC = storyboard!.instantiateViewController(identifier: "ReviewViewController", creator: { coder in
return ReviewViewController(coder: coder, taskListDataSource: taskListDataSource)
})
addVC = storyboard!.instantiateViewController(identifier: "AddViewController")
setViewControllers([todayVC], direction: .forward, animated: false)
dataSource = self
print(plannerVC.tableView) // Console is printing nil
}
When you call instantiateInitialViewController(creator:), the UIViewController is initiated, but its view (and all subviews, including then all the IBOutlet) aren't loaded in memory.
So when, you try to do self.someIBoutlet (in your case self.plannerVC.tableView.reloadData(), it crashes.
A solution, would be to force the view to load, with loadViewIfNeeded().
Since loading the view can be heavy, it's usually used when the ViewController will be shown shortly after (for instance, in a didSet of some property that access outlet in it, because it will be shown on screen in a few instants, so the view will be loaded anyway, just a few moment after).
Since you are loading 3 UIViewController, could it be that you aren't showing them, but prematurely loading them?
If that's the case, you might rethink your app architecture (all your UIViewController don't need to be initialized and in memory, and less to have their view loaded).
Still, you can check beforehand if the view has been loaded, and that you can access the outlets with isViewLoaded.
I'dd add for that a method in PlannerVC:
func refreshData() {
guard isViewLoaded else { return }
tableView.reloadData()
}
Side note, it could be a protocol (and even more, complexe, like adding var tableView { get set }, and have a default implementation of refreshData(), but that's going further, not necessary)...
protocol Refreshable {
func refreshData()
}
let taskListDataSource = TaskListDataSource {
self.todayVC.refreshData()
self.plannerVC.refreshData()
self.reviewVC.refreshData()
}
Side note, I would check if there isn't memory retain cycles, I would have use a [weak self] in the closure of TaskListDataSource, and also would have made it a property of the VC.
I have a UITabBarController (as initial view controller) which checks connectivity status of the device. Everytime the connectivity status changes, a checkmark in a child UITableViewController cell (.accessoryType) should be set (.checkmark) or removed (.none)
Code in Tab Bar Controller:
let monitor = NWPathMonitor()
monitor.pathUpdateHandler = { path in
let tvc = InfoTableViewController()
if path.status == .satisfied {
// set .checkmark in UITableViewController
let cell = tvc.statusOnlineCell
print("cell :", cell)
} else {
// set .none in UITableViewController
}
}
let queue = DispatchQueue.global(qos: .background)
monitor.start(queue: queue)
Outlet in UITableViewController:
#IBOutlet weak var statusOnlineCell: UITableViewCell!
I can change the accessory type from inside the UITableViewController class using .checkmark and .none.
statusOnlineCell.accessoryType = .checkmark
statusOnlineCell.accessoryType = .none
All fine so far!
However as soon as I try to access the UITableView.statusOnlineCell from UITabBar, I get nil. Hence, I cannot change its property outside the UITableViewController.
I saw 3 possible approaches:
A global variable, which reflects the online status. I could use the UITableView.viewDidAppear() method to change the statusOnlineCell accessory type. This works, but only if UITableView is not shown (only if another than UITableView is shown). If the UITableView is shown and I change the connectivity status, the view is not reloaded and I didn't find any way to achieve this. Is this possible?
Find a possibility to change the accessory type of UITableView.statusOnlineCell from the UITabBarController. Accessing the outlook returned in nil. Why is that? On top, after the accessory type would have changed, I would need to reload the view (for the case that the UITableView was active while changing connectivity status).
Is there any kind of (unknown to me) method which fires, when an object's property changed (à la needReload())? This would be too good to be true I believe.
To summarize - I need code to change the accessory type of a tableview cell, depending on the connectivity status, even whith this tableview being visible.
I read some tutorials and stackexchange articles, Google, ... but none did the job.
This was my top candidate, but I didn't manager to apply these examples to my situation.
https://learnappmaking.com/pass-data-between-view-controllers-swift-how-to/#back-delegation
I didn't want to use notifications since not really appropriate.
Any hint would be sufficient. Thanks in advance.
----- EDIT (14:52 UTC) ----- (requested by #vadian)
I added the (testing) code in the UITabBarController. The result of cell shows nil. So I cannot directly address the cell in UITableView from UITabBarController.
The UITableView doesn't have any related code yet since I directly address the property statusOnlineCell from UITabBarController in order to change its accessory type.
This approach is option 2. (of my 3 possibilities mentioned above).
I didn't find a solution, but a workaround.
Why no solution : the cell object was always nil if called from another class. Hence no chance to change it's property to .checkmark or .none. This was basically my key problem and unknown to me.
The workaround : a delegate! I declared the class, holding the cell, as delegate for the connectivity status change. Like that, the checkmark gets set or is removed from the cell, independantly whether the page is in view or not.
This runs when the app starts (UITabBarController):
protocol StatusOnlineChangedDelegate {
func updateStatusOnline(_ online: Bool)
}
class TabBarController: UITabBarController {
var socDelegate: StatusOnlineChangedDelegate?
override func viewDidLoad() {
super.viewDidLoad()
let monitor = NWPathMonitor()
monitor.pathUpdateHandler = { path in
if path.status == .satisfied {
self.socDelegate?.updateStatusOnline(true)
} else {
self.socDelegate?.updateStatusOnline(false)
}
}
let queue = DispatchQueue.global(qos: .background)
monitor.start(queue: queue)
}
}
The UITableViewController holding the cell looks like this:
class InfoTableViewController: UITableViewController, StatusOnlineChangedDelegate {
#IBOutlet weak var statusOnlineCell: UITableViewCell!
override func viewDidLoad() {
super.viewDidLoad()
TabBarController().socDelegate = self
}
func updateStatusOnline(_ online: Bool) {
DispatchQueue.main.async {
self.statusOnlineCell.accessoryType = (online ? .checkmark : .none)
}
}
}
Runs perfectly as desired.
I am relatively new on firebase. I want to update existing data on my firebase database. I have UITableView and UITableViewCells on my Xcode project, when user touch (tap gesture), for example a outlet on cell view, I want to update Firebase Database but this could be any cell on tableview. How to find this cell which user touched on screen, on firebase database and update its messageVoteScore value. There are assigned keys but my cells do not know those keys,
I could not figure it out how to match them.(Database/Messages/{"sender": "email"},{"messageBody":"text.."},{"messageVoteScore":"100"}
#objc func voteUpTapped(sender: UITapGestureRecognizer) {
// Update score //
self.messageVoteScore.text = String(Int(self.messageVoteScore.text!)! + 1 )
//observeSingleEventOfType listens for a tap by the current user.
Database.database().reference().child("Messages").observe(.value){
(snapshot) in
if let snapshotValue = snapshot.value as? [String : String] {
print(snapshotValue)
}
}
}
Are you passing class objects to your cell? If so you could use a protocol/delegate for this. A UITableView Cell is a View and shouldn't function as a controller. your UITableViewController should be updating firebase.
you should start by adding something like this to the top of your Cell class:
protocol MyTableViewCellDelegate: class {
func thingDidDo(object: Object, text: String...) //object is the class object you are changing.
then add the delegate to your properties:
weak var delegate: MyTableViewCellDelegate?
then add the IBAction:
#IBAction func thingDidDo(_ sender: Any) {
delegate?.thingDidDo(object: object, text: textView.text...)
}
Now back to your viewController Class:
In your "cellForRowAt" write in the delegate:
cell.delegate = self
then add an extension
extension MyTableViewController: MyTableViewCellDelegate {
func thingDidDo(object: Object, text: String) {
Do whatever you want with object, text...
}
Not sure what kind of function you are trying to perform so I made this as generic as I could
I have a UITableView that gets populated by the following firebase database:
"games" : {
"user1" : {
"game1" : {
"currentGame" : "yes",
"gameType" : "Solo",
"opponent" : "Computer"
}
}
}
I load all the games in viewDidLoad, a user can create a new game in another UIViewController, once a user does that and navigates back to the UITableView I want to update the table with the new row. I am trying to do that with the following code:
var firstTimeLoad : Bool = true
override func viewDidLoad() {
super.viewDidLoad()
if let currentUserID = Auth.auth().currentUser?.uid {
let gamesRef =
Database.database().reference().child("games").child(currentUserID)
gamesRef.observe(.childAdded, with: { (snapshot) in
let game = snapshot
self.games.append(game)
self.tableView.reloadData()
})
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if firstTimeLoad {
firstTimeLoad = false
} else {
if let currentUserID = Auth.auth().currentUser?.uid {
let gamesRef = Database.database().reference().child("games").child(currentUserID)
gamesRef.observe(.childAdded, with: { (snapshot) in
self.games.append(snapshot)
self.tableView.reloadData()
})
}
}
}
Lets say there is one current game in the data base, when viewDidLoad is run the table displays correctly with one row. However anytime I navigate to another view and navigate back, viewDidAppear is run and for some reason a duplicate game seems to be appended to the games even though no child is added.
The cells are being populated by the games array:
internal func tableView(_ tableView: UITableView, cellForRowAt indexPath:
IndexPath) -> UITableViewCell {
let cell = Bundle.main.loadNibNamed("GameTableViewCell", owner:
self, options: nil)?.first as! GameTableViewCell
let game = games[indexPath.row]
if let gameDict = game.value as? NSDictionary {
cell.OpponentName.text = gameDict["opponent"] as? String
}
return cell
}
UPDATE:
Thanks to everyone for their answers! It seems like I misunderstood how firebase .childAdded was functioning and I appreciate all your answers trying to help me I think the easiest thing for my app would be to just pull all the data every time the view appears.
From what I can see, the problem here is that every time you push the view controller and go back to the previous one, it creates a new observer and you end up having many observers running at the same time, which is why your data appears to be duplicated.
What you need to do is inside your viewDidDisappear method, add a removeAllObservers to your gameRef like so :
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
guard let currentUserId = Auth.auth().currentUser?.uid else {
return
}
let gamesRef = Database.database().reference().child("games").child(currentUserId)
gamesRef.removeAllObservers()
}
I cannot see all your code here so I am not sure what is happening, but before adding your child added observer, you need to remove all the elements from your array like so :
games.removeAll()
Actually, as per best practices, you should not call your method inside your ViewDidLoad, but instead you should add your observer inside the viewWillAppear method.
I cannot test your code right now but hopefully it should work like that!
Let me know if it doesn't :)
UPDATE:
If you want to initially load all the data, and then pull only the new fresh data that is coming, you could use a combination of the observeSingleEvent(of: .value) and observe(.childAdded) observers like so :
var didFirstLoad = false
gamesRef.child(currentUserId).observe(.childAdded, with: { (snapshot) in
if didFirstLoad {
// add your object to the games array here
}
}
gamesRef.child(currentUserId).observeSingleEvent(of: .value, with: { (snapshot) in
// add the initial data to your games array here
didFirstLoad = true
}
By doing so, the first time it loads the data, .childAdded will not be called because at that time didFirstLoad will be set to false. It will be called only after .observeSingleEvent got called, which is, by its nature, called only once.
Try following code and no need to check for bool , Avoid using bool here its all async methods , it created me an issue in between of my chat app when its database grows
//Remove ref in didLoad
//Remove datasource and delegate from your storyboard and assign it in code so tableView donates for data till your array don't contain any data
//create a global ref
let gamesRef = Database.database().reference()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.games.removeAllObjects()
if let currentUserID = Auth.auth().currentUser?.uid {
gamesRef.child("games").child(currentUserID)observe(.childAdded, with: { (snapshot) in
self.games.append(snapshot)
self.tableView.dataSource = self
self.tableView.delegate = self
self.tableView.reloadData()
})
gamesRef.removeAllObserver() //will remove ref in disappear itself
//or you can use this linen DidDisappear as per requirement
}
else{
//Control if data not found
}
}
//TableView Delegate
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if self.games.count == 0{
let emptyLabel = UILabel(frame: CGRect(x: 0, y: 0, width: self.view.bounds.size.width, height: self.view.bounds.size.height))
emptyLabel.text = "No Data found yet"
emptyLabel.textAlignment = .center
self.tableView.backgroundView = emptyLabel
self.tableView.separatorStyle = .none
return 0
}
else{
return self.games.count
}
}
observe(.childAdded) is called at first once for each existing child, then one time for each child added.
Since i also encounter a similar issue, assuming you don't want to display duplicate objects, in my opinion the best approach, which is still not listed in the answers up above, is to add an unique id to every object in the database, then, for each object retrieved by the observe(.childAdded) method, check if the array which contains all objects already contains one with that same id. If it already exists in the array, no need to append it and reload the TableView. Of course observe(.childAdded) must also be moved from viewDidLoad() to viewWillAppear(), where it belongs, and the observer must be removed in viewDidDisappear. To check if the array already includes that particular object retrieved, after casting snapshot you can use method yourArray.contains(where: {($0.id == retrievedObject.id)}).
I was working on a grocery list app. When I tried to synchronise data to table view using Firebase, the grocery item can not be shown on the table view. However, when I reset the simulator, items added before can be shown on the table view, but when I add another item, it can't be shown on the table view. I double checked the code and found nothing wrong. Could you help me fix it?(e.g. In the following two images, the coffee item can not be shown)
Part of code in GroceryListViewController.swift
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
//Attach a listener to receive updates.
ref.observeSingleEventOfType(.Value, withBlock: { (snapshot) in
var newItems = [GroceryItem]()
for item in snapshot.children
{
let groceryItem = GroceryItem(snapshot: item as! FDataSnapshot)
newItems.append(groceryItem)
}
self.items = newItems
self.tableView.reloadData()
})
}
1 - You are using observeSingleEventOfType (* This is equivalent to observeEventType:withBlock:, except the block is immediately canceled after the initial data is returned.) From the doc. So your listener is called once and stop listening. Use observeEventType instead
2 - You should do the reloadData of your tableview on the main thread like this :
NSOperationQueue.mainQueue().addOperationWithBlock({
self.tableView.reloadData()
})
The reason is that you cannot / must not interact with UI from a background thread. Since Firebase is asynchronous the completion block is on a background thread
Maybe it's obvious, but it happen to me. I was debugging and the code was stopped in a breakpoint, and data was not in the console in firebase. Then I realized that I had to let it run completely to refresh. Maybe it can help...
Add this code once touch the button "+":
self.items.append(groceryItem)
let row = self.items.count - 1
let indexPath = NSIndexPath(forRow: row, inSection: 0)
self.tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Fade)
This ensures the interaction with UI.