I am new to programming and currently trying to make a newsfeed like app. My goal is it to notice changes in the firebase database through my app and new posts in my table view. Currently I am able to show one post in my tableView. Its only changing though when you reopen the ViewController.
One of my problem is that I don't know how to use any other command then obserSingleEvent and I am pretty sure that one of my main mistakes.
So my questions are:
1. How do I make the tableView instantly reload when a change appears?
2. how do I display more then one post?
(3. how can I show the most recent posts first?)
class ViewController: BaseViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet var newsfeedTableView: UITableView!
var ref: DatabaseReference!
var posts = [String]()
override func viewDidLoad() {
super.viewDidLoad()
addSlideMenuButton()
ref = Database.database().reference()
ref.child("posts").queryOrderedByKey().observeSingleEvent(of: .childAdded, with: { snapshot in
let newPost = snapshot.value as? String
self.posts.append(newPost!)
print("newPost: \(newPost)")
print("posts: \(self.posts)")
self.newsfeedTableView.reloadData()
print("reloadData")
})
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return (posts.count)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: UITableViewCell.CellStyle.default, reuseIdentifier: "cell")
cell.textLabel?.text = posts[indexPath.row]
print("posts in cell: \(posts)")
return cell
}
}
You're observing with singleEvent which is wrong for the intended use.
ref.child("posts").observe( .childAdded, with: { snapshot in
let newPost = snapshot.value as? NSDictionary
let post_title:String = newPost!["post_title"] as? String ?? "error"
self.posts.append(post_title)
print("newPost: \(newPost)")
print("posts: \(self.posts)")
self.newsfeedTableView.reloadData()
print("reloadData")
})
Also, that sorting system won't work for a news app. you can't work with human-provided 1,2,3. could you update it to something like this from now on:
(1 - 2) A random-unique generated post ID
PS. It's impossible for Firebase to order your post list with post, post1, post2. So in this case queryOrderedByKey() is useless.
Making the posts 1,2,3 would work for the ordering system
1) You can implement a function that is constantly observing the data in Firebase for changes and update the array. But be careful with that, it will be consuming memory and also you will get a lot of network usage.
.observe // instead of .observeSingleEvent will do that
Again, it would be better if you add a refresher to the tableView so you fetch the data on demand instead of all the time.
2) Your array is fine. I'll move the call to firebase to another function instead of having it in viewDidLoad().
3) You need to store a timeStamp in firebase to get this functionality. Yes, you can queryOrdered but remember firebase is async. Data will be given as available so it would be better to sort the array depending on the creation timeStamp of each of the items.
Related
I'm trying to make an app, in which one of the functions is, that in one viewController, the user can enter some data about a given person (a photo, a name and an answer to a certain question). When pressing the "save new person-button", the user is to be redirected to the previous viewController, which amongst other things holds a tableView.
In this tableView, I want there to be created a new cell for each time the user presses the "save new person-button" in the other viewController. The cell should, of course, hold the person's name (and ideally also a miniature of the photo).
Now, it is important that the data is stored 'internally' - also after exiting a certain viewController or closing the app. Therefore I'm using Realm as a database to store all the data.
I'm very new when it comes to using Realm, so maybe my questions seem stupid. Basically, I think I have been able to make the "Save new person-button" save all the data in the realm database. But when trying to make the button create new cells (which must stay, once created!!) in the previous viewController... All kinds of weird things happen!
What the code underneath shows is my current attempt on passing at least the name through a segue, but that creates several problems: The cells don't stay. If you try to add a new cell, it just overrides the old one. And finally, the PeopleData.count apparently counts the number of letters in the name - and creates fx. 5 identical cells for a 5 letter long name! :D
I'm sure this is not the right way to pass the data ... So what is the best way to present this data? So that the cells stay once created - and so that the user can add several new people/several new cell without overriding the old ones!
Relevant code:
viewController where the user can enter the data:
#IBAction func SaveNewPerson() {
let NewPerson = person()
NewPerson.name = PersonName.text!
NewPerson.answer = PersonAnswer.text!
NewPerson.extraIdentifier = PersonExtraIdentifier.text!
let realm = try! Realm()
try! realm.write {
realm.add(NewPerson)
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// let allPeople = realm.objects(person.self)
let SomePersonName = PersonName.text!
if segue.identifier == "PassingSomeInfo" {
if let vc = segue.destination as? AllPersonsInYourDiary {
vc.PeopleData = SomePersonName
}
}
Viewcontroller with the tableView:
var PeopleData: ""
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return PeopleData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Person1", for: indexPath)
cell.textLabel?.text = self.PeopleData
cell.textLabel?.numberOfLines = 0
cell.textLabel?.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.headline)
return cell
}
Third viewController (used to store all the person-data):
class person: Object {
#objc dynamic var name = ""
#objc dynamic var answer = ""
#objc dynamic var extraIdentifier = "" }
Thanks!
This question is honestly a little to broad. What you are asking for are several things at once. And if I were to write the implementation for you I wouldn't answer your actual question on what you need to do. What I think you are asking is this:
How do I write and retrieve data from realm.
How do I structure my code for passing data between views.
So my recommendation is this.
Don't pass the data with a segue. Create a class that will access Realm and retrieve the data you need for that view. Name it something like Database, Realm or LocalStorage.
In that class add methods for writing to Realm and retrieving from Realm. How to do that you can find in the Realm documentation: https://realm.io/docs/swift/latest/
In your view that needs to display the entries instantiate your Realm class. Some psuedo code to help you on the way:
class ViewController {
fileprivate let database = LocalStorage()
fileprivate var peopleData: [People]()
viewDidLoad() {
super.viewDidLoad()
database.retrieveData(primaryKey: "extraIdentifier", completionHandler: { entries, err in
if err == nil {
self.peopleData = entries
}
})
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return peopleData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) ->
UITableViewCell {
let person = self.peopleData[indexPath.item]
let cell = tableView.dequeueReusableCell(withIdentifier: "Person1", for: indexPath)
cell.textLabel?.text = person.name
cell.textLabel?.numberOfLines = 0
cell.textLabel?.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.headline)
return cell
}
}
After you call
try! realm.write {
realm.add(NewPerson)
}
you already saved on realm so you don't have to pass this on the segue you can retrieve from realm on the next view controller by calling
realm.objects(ofType: NewPerson.self)
This will return an array of NewPerson or you can also search just one with your primary key
realm.object(ofType: NewPerson.self, forPrimaryKey: yourKey)
How do you sort table view cells based on the value in the detail text label? I have names and scores in Firebase that I'm pulling out and want to show in order of the highest to lowest score.
Currently, I'm retrieving the Firebase names and values, but they are just displayed by alphabetical order of the names in the main text field. I've tried sorting by the score values with {$0.scoreTotal > $1.scoreTotal} and ordering the Firebase data with queryOrdered(byChild: "scoreTotal"), but neither seems to work.
Here's the table view section of code:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CellMale", for: indexPath as IndexPath)
let comp = comps[indexPath.row]
self.comps.sorted(by: {$0.scoreTotal > $1.scoreTotal})
let dbRef1 = FIRDatabase.database().reference().child("Live Scoring Male/\(String(describing: comp.name!))/scoreTotal")
dbRef1.queryOrdered(byChild: "scoreTotal").observe(FIRDataEventType.value, with: { (snapshot) in
let scoreTotal = snapshot.value as! Double
cell.detailTextLabel?.text = String(scoreTotal)
})
cell.textLabel?.text = comp.name
return cell
}
First thing is never call any services into cellForRowAtIndex and call it when it needed like while we load view use viewDidLoad() method for loading any data into that Controller.
so call above method into viewDidLoad and regarding sorting your data
use FireBase sorting like below:
let ref = FIRDatabase.database().reference(fromURL: <FIREBASE_URL>).child("topics")
let query = ref.queryOrdered(byChild: "scores")
query.observe(.value, with: { (snapshot) in
for childSnapshot in snapshot.children {
print(childSnapshot)
}
})
catch this into Local Array or Dictionary and use it in your TableView with reloadData() method.
I'm using firebase realtime database and it's working fine for some parts of my app. I was going through a tutorial on youtube which populates a collectionView with users. It uses NSDictionary to get the photo URL and username and puts them in the collection view for all users. I deleted some of the users directly in the firebase console, and now have only one user. For some reason it's still pulling the users that I deleted. This is the collectionView file.
import UIKit
import FirebaseDatabase
private let reuseIdentifier = "UserSearchCell"
class UsersCollectionViewController: UICollectionViewController {
var usersDict = NSDictionary?()
var userNamesArray = [String]()
var userImagesArray = [String]()
override func viewDidLoad() {
super.viewDidLoad()
DataService.ds.REF_USERS.observeEventType(.Value, withBlock :{
(snapshot) in
self.usersDict = snapshot.value as? NSDictionary
for(userId,details) in self.usersDict!{
let img = details.objectForKey("profileThumbUrl") as! String
let name = details.objectForKey("username") as! String
self.userImagesArray.append(img)
self.userNamesArray.append(name)
self.collectionView?.reloadData()
}
})
self.collectionView!.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
}
override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.userImagesArray.count
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! UserSearchCell
let imageUrl = NSURL(string:userImagesArray[indexPath.row])
let imageData = NSData(contentsOfURL: imageUrl!)
cell.userImage.image = UIImage(data:imageData!)
cell.userName.text = userNamesArray[indexPath.row]
return cell
}
}
Shouldn't this always sync with the database? Where are these deleted users coming from? Also I didn't include the cell code because all it is is the imageView and Label actions being declared. I ran into this problem before but my memory is bad and I don't remember why it was doing it or if I ever solved it.
Okay I figured out the answer to my own question. Apparently the simulator has a problem with firebase and seems to form some sort of cache of firebase data. I tried running it on a phone I had been testing on and it didn't have this cache and everything worked fine. Also I tried running on a different device IN the simulator and it worked fine there too. So I want to leave this up because I think a lot of people may have trouble with Firebase since it's core feature doesn't work well with the IOS simulator.
I am having trouble reloading the tableView. When my firebaseService.getAllPosts() method runs, it gives me the two posts in the database (as intended). I can tell this because when the didSet print runs, it gives me the correct count. However, because I know the tableview is set before the method gets run, I know I need to reload the tableview to update the count in my datasource. There-in lies the issue. I put the posts variable outside of my class on purpose so it could be accessed from every class. (If this is not a good practice, let me know.)
Where can I run tableView.reloadData() so my tableView datasource updates and gives me the correct could of posts? I've tried putting FeedController().tableView.reloadData() in the didSet and I've tried putting it in viewDidLoad() but neither of these worked. I've also tried adding a variable called _posts and setting it equal to posts and adding didSet to that with tableView.reloadData() inside the didSet but that doesn't work either.
class FeedController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let tableView = UITableView(frame: UIScreen.mainScreen().bounds, style: UITableViewStyle.Plain)
let cellId = "PhotoCell"
let textCellId = "TextCell"
let firebaseService = FirebaseService.sharedInstance
static let sharedFeedInstance = FeedController()
var posts = [Post]() {
didSet {
tableView.reloadData()
print(posts.count)
}
}
override func viewDidLoad() {
super.viewDidLoad()
firebaseService.getAllPosts()
tableView.dataSource = self
tableView.delegate = self
self.view.addSubview(tableView)
}
// MARK: - Table view data source
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
print("sections: \(posts.count)")
return posts.count
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return 1
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let post:Post? = posts[indexPath.section]
if let _ = post?.imageContentName {
let photoFeedCell = tableView.dequeueReusableCellWithIdentifier(self.cellId, forIndexPath: indexPath) as? FeedTVCellWithPhoto
photoFeedCell?.post = post
return photoFeedCell!
}
let textFeedCell = tableView.dequeueReusableCellWithIdentifier(self.textCellId, forIndexPath: indexPath) as? FeedTVCellText
textFeedCell?.post = post
return textFeedCell!
}
}
Update 1: getAllPosts method from FirebaseService class
func getAllPosts() {
let postRef = ref.child("posts")
postRef.observeSingleEventOfType(.Value, withBlock: { snapshot in
// print(snapshot.value)
if let snapshots = snapshot.children.allObjects as? [FIRDataSnapshot] {
for snap in snapshots {
if let postDictionary = snap.value as? Dictionary<String, AnyObject> {
let key = snap.key
let post = Post(key: key, dictionary: postDictionary)
FeedController.sharedFeedInstance.posts.insert(post, atIndex: 0)
}
}
}
})
}
In essence, you are addressing the methods and properties of FeedController the wrong way. Every time that you use FeedController() in your code you should be using self.
The following two paragraphs are a refresher about class and instance methods and properties. Feel free to ignore them if it's something that's already in your radar:
If a method or property belongs to an instance (that is, it may vary from instance to instance) you call it with self.property or self.method(), although often you can drop the self. as long as it does not create ambiguity.
If a method or property belongs to the whole class (and therefore it is declared as static let, static var or static func) then you would call it with NameOfTheClass.property or NameOfTheClass.method(). In your case, you would call it FeedController.property or FeedController.method(). That is, without the trailing parentheses. In any case, class methods and properties should be used sparingly and would probably not be appropriate in this situation.
Having settled that, we have the question of how to reload the data. If you move posts inside FeedController (making the array an instance variable) and add tableView.reloadData() (without the self.) to the didSet() of posts, you should be good, notwithstanding other unrelated code smells that others have highlighted and you should try to fix, but those should not affect your ability to reload the data.
So off the bat:
"FeedController().tableView.reloadData()" is
instantiating another FeedController. That's not going to work. Where to put the reload call will depend on your requirements. For example if you have a lot of segues occurring where you go back and forth to this UIViewController and the data is being changed in other UIViewControllers, you can reload in an override of viewWillAppear() or the other lifecycle methods. You need to call that on the tableView for the instance of the UIViewController that is loaded into the UIWindow, not what you're doing now.
Any reason why you're doing a lot of this stuff programmatically rather
than through the storyboard? Either way is fine but I find storyboard
to be a nice separation. For example if you used a UITableViewController through storyboard you could have it set up the delegate and dataSource without doing it through code which just looks like ugly boilerplate. You can set these without using a UITableViewController as well, as long as you meet the protocols. Those things that can be handled in the storyboard should is what I usually go by.
Your class is extending UIViewController and implementing those two protocols, why not just extend a UITableViewController? Unless you're manually putting a UITableView on there and need to show something other than a UITableView on that UIViewController there's no reason I can think of to do what you're doing.
Something else to note is that the way you're doing the PhotoFeedCell and TextFeedCell is kind of weird and gives mixed signals. You're dequeuing a reusable cell and optional casting it and using optional chaining afterwards, both of which implies that you're accepting it may be nil, but then force unwrapping it right after.
First of all, I've looked around to find a solution to my problem, here and on other websites. If I've missed something please show me the link, i didn't intend on bugging you with my problem if there is a solution somewhere else.
My idea was to create an app (just for myself as a practise since I'm fairly new to swift) that would get the NBA schedule from a website, extract the games and results and show them in a table. For that I made a textField where the user could enter from which game day he wanted the results. The Integer he enters changes the url and the url is propperly spilt up and the data I want to display is saved in an array as a string.
Thats were my problem occurs. The items are appended to the array and the array.count displays the right number depending on the day the user entered. The only problem is that the data from the array is not display in the table cell. I've rewrote the code and made sure I didn't mess up the table, but as soon as I add the second part of the app (the information that got from the URL) to the app, the cells don't display anything.
It's kind of weird because both parts are working fine on their own, but as soon as I combine them my problem occurs. Do you know where I might have messed up?
Does anyone have an idea what my mistake may be? I'm not looking for code solutions, just for someone who might tell me where the flaw in my logic is. Maybe i missed something, but i don't get why my cells are not displaying the elements of the array, even though the array is set up properly.
Thanks in advance to anyone answering and have a nice day!
Greetings!
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var gamesArray = [String]()
var gameDay:Int = 0
#IBOutlet var textField: UITextField!
#IBOutlet var gamesTable: UITableView!
#IBAction func enterButton(sender: AnyObject) {
gameDay = Int(textField.text!)!
// webCodeArrayForGames is where i temporarily put the strings I want to add
for var counter = 1; counter<webCodeArrayForGames.count; counter++{
self.gamesArray.append(webCodeArrayForGames[counter])
}
override func viewDidLoad()
{
super.viewDidLoad()
gamesTable.delegate = self
gamesTable.dataSource = self
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return gamesArray.count
}
func TableView(tableView: UITableView, cellforRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{
let cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "cell")
cell.textLabel?.text = gamesArray[indexPath.row]
return cell
}
override func viewDidAppear(animated: Bool) {
self.gamesTable.reloadData()
}
}
You can check out this one:
https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/TableView_iPhone/TableViewAPIOverview/TableViewAPIOverview.html
On the short, you have to provide UITableViewDelegate and UITableViewDataSource.
From the code, I suspect that you didn't provide the dataSource for your table:
"The data source adopts the UITableViewDataSource protocol. UITableViewDataSource has two required methods. The tableView:numberOfRowsInSection: method tells the table view how many rows to display in each section, and the tableView:cellForRowAtIndexPath: method provides the cell to display for each row in the table. "
You could do this way:
Subclass your viewController from UITableViewDelegate and UITableViewDataSource
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { ....
in viewDidLoad() assign the tableview's delegate and dataSource:
override func viewDidLoad() {
super.viewDidLoad()
gamesTable.delegate = self
gamesTable.dataSource = self ....
}