I have this closure which I use to populate my array and dictionary. However, when I try to use it outside the function, it's empty. I understand that this closure works on an asynchronous thread, so is it right to assume that I try to access that variable before it's been populated? As a result, I get an empty array. Here is my code.
class HomeCollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout, UISearchBarDelegate, UIGestureRecognizerDelegate {
var entries = [String: DiaryEntry]()
var entryIDS = [String]()
var searchController: UISearchController!
override func viewDidLoad() {
super.viewDidLoad()
// Register cell classes
self.collectionView!.register(DiaryCell.self, forCellWithReuseIdentifier: "homeCell")
collectionView?.backgroundColor = UIColor.white
navigationController?.hidesBarsOnSwipe = true
if let userID = FIRAuth.auth()?.currentUser?.uid {
FirebaseService.service.getUserEntriesRef(uid: userID).observe(.value, with: { [weak weakSelf = self] (snapshot) in
let enumerator = snapshot.children
while let entry = enumerator.nextObject() as? FIRDataSnapshot {
weakSelf?.entryIDS.append(entry.key)
weakSelf?.entries[entry.key] = DiaryEntry(snapshot: entry)
}
weakSelf?.entryIDS.reverse()
weakSelf?.collectionView?.reloadData()
})
print("Entries: \(entryIDS.count) ")
}
// Do any additional setup after loading the view.
}
What's the best way to deal with such a multithreaded execution?
I follow the coding standards (for Swift) of Raywenderlich and his team. If I'm going to re-write your code to have a strong self, it would be like this:
class HomeCollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout, UISearchBarDelegate, UIGestureRecognizerDelegate {
var entries = [String: DiaryEntry]()
var entryIDS = [String]()
var searchController: UISearchController!
override func viewDidLoad() {
super.viewDidLoad()
// Register cell classes
self.collectionView!.register(DiaryCell.self, forCellWithReuseIdentifier: "homeCell")
collectionView?.backgroundColor = UIColor.white
navigationController?.hidesBarsOnSwipe = true
if let userID = FIRAuth.auth()?.currentUser?.uid {
FirebaseService.service.getUserEntriesRef(uid: userID).observe(.value, with: {
[weak self] (snapshot) in
guard let strongSelf = self else {
return
}
let enumerator = snapshot.children
while let entry = enumerator.nextObject() as? FIRDataSnapshot {
strongSelf.entryIDS.append(entry.key)
strongSelf.entries[entry.key] = DiaryEntry(snapshot: entry)
}
strongSelf.entryIDS.reverse()
strongSelf.collectionView?.reloadData()
})
print("Entries: \(entryIDS.count) ")
}
// Do any additional setup after loading the view.
}
I hope this works. I'm writing my code like this too when connecting to any APIs, such as Firebase and Alamofire.
Use Dispatch Groups to keep track of when you're done appending all the elements to your array, then make a notify callback that'll automatically be called when they're all added.
class HomeCollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout, UISearchBarDelegate, UIGestureRecognizerDelegate {
var entries = [String: DiaryEntry]()
var entryIDS = [String]()
let dispatchGroup = DispatchGroup()
var searchController: UISearchController!
override func viewDidLoad() {
super.viewDidLoad()
// Register cell classes
self.collectionView!.register(DiaryCell.self, forCellWithReuseIdentifier: "homeCell")
collectionView?.backgroundColor = UIColor.white
navigationController?.hidesBarsOnSwipe = true
self.dispatchGroup.enter()
if let userID = FIRAuth.auth()?.currentUser?.uid {
FirebaseService.service.getUserEntriesRef(uid: userID).observe(.value, with: { [weak weakSelf = self] (snapshot) in
let enumerator = snapshot.children
while let entry = enumerator.nextObject() as? FIRDataSnapshot {
weakSelf?.entryIDS.append(entry.key)
weakSelf?.entries[entry.key] = DiaryEntry(snapshot: entry)
}
self.dispatchGroup.leave()
})
self.dispatchGroup.notify(queue: DispatchQueue.main, execute: {
print("Entries: \(entryIDS.count) ")
weakSelf?.entryIDS.reverse()
weakSelf?.collectionView?.reloadData()
})
}
// Do any additional setup after loading the view.
}
I'd also recommend just using self instead of weakSelf?.
I just came across your problem, while searching for a solution of the same problem and i finally managed to figure out. so, i will try to answer it would be helpful for many others coming behind us.
The problem is that the array exists only inside the closure. So, the solution is to make an array outside of viewDidLoad and set it using once you have the complete array, then use didSet to set entryIDS
var entryIDS = [String]() {
didSet {
//something
}
}
Related
I'm trying to learn iOS programming so I thought it would be a good idea to emulate instagrams feed. Everyone uses this basic feed and I would like to know how to do it.
The basic idea is to have one image/text post show up in a single column. Right now I have a a single image to be shown.
I'm currently extracting the image url correctly from firebase. The only issue is that my CollectionView still is showing up empty. I started this project months ago and I forget where the tutorial is at. Please help me fill in the blanks. Here is the code:
import UIKit
import SwiftUI
import Firebase
import FirebaseUI
import SwiftKeychainWrapper
class FeedViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource{
#IBOutlet weak var collectionview: UICollectionView!
//var posts = [Post]()
var posts = [String](){
didSet{
collectionview.reloadData()
}
}
var following = [String]()
var posts1 = [String]()
var userStorage: StorageReference!
var ref : DatabaseReference!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
posts1 = fetchPosts()
//let myIndexPath = IndexPath(row: 0, section: 0)
//collectionView(collectionview, cellForItemAt: myIndexPath)
//print(self.posts1.count)
}
func fetchPosts() -> [String]{
let uid = Auth.auth().currentUser!.uid
let ref = Database.database().reference().child("posts")
let uids = Database.database().reference().child("users")
uids.observe(DataEventType.value, with: { (snapshot) in
let dict = snapshot.value as! [String:NSDictionary]
for (_,value) in dict {
if let uid = value["uid"] as? String{
self.following.append(uid)
}
}
ref.observe(DataEventType.value, with: { (snapshot2) in
let dict2 = snapshot2.value as! [String:NSDictionary]
for(key, value) in dict{
for uid2 in self.following{
if (uid2 == key){
for (key2,value2) in value as! [String:String]{
//print(key2 + "this is key2")
if(key2 == "urlToImage"){
let urlimage = value2
//print(urlimage)
self.posts1.append(urlimage)
self.collectionview.reloadData()
print(self.posts1.count)
}
}
}
}
}
})
})
//ref.removeAllObservers()
//uids.removeAllObservers()
print("before return")
print(self.posts1.count)
return self.posts1
override func viewDidLayoutSubviews() {
collectionview.reloadData()
}
func numberOfSections(in collectionView: UICollectionView) ->Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return posts1.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PostCell", for: indexPath) as! PostCell
cell.postImage.sd_setImage(with: URL(string: posts1[indexPath.row]))
//creating the cell
//cell.postImage.downloadImage(from: self.posts[indexPath.row])
// let storageRef = Storage.storage().reference(forURL: self.posts[indexPath.row].pathToImage)
//
//
print("im trying")
//let stickitinme = URL(fileURLWithPath: posts1[0])
//cell.postImage.sd_setImage(with: stickitinme)
//cell.authorLabel.text = self.posts[indexPath.row].author
//cell.likeLabel.text = "\(self.posts[indexPath.row].likes) Likes"
return cell
}
#IBAction func signOutPressed(_sender: Any){
signOut()
self.performSegue(withIdentifier: "toSignIn", sender: nil)
}
#objc func signOut(){
KeychainWrapper.standard.removeObject(forKey:"uid")
do{
try Auth.auth().signOut()
} catch let signOutError as NSError{
print("Error signing out: %#", signOutError)
}
dismiss(animated: true, completion: nil)
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
}
*/
}
UPDATE
The observe call is not updating the value of posts (the dictionary). Once the observe call exits, the value of posts is set back to empty.
PostCell class as asked:
import UIKit
class PostCell: UICollectionViewCell {
#IBOutlet weak var postImage: UIImageView!
#IBOutlet weak var authorLabel: UILabel!
#IBOutlet weak var likeLabel:UILabel!
#IBOutlet weak var likeBtn:UIButton!
#IBOutlet weak var unlikeBtn:UIButton!
#IBAction func likePressed (_ sender: Any){
}
#IBAction func unlikePressed(_sender: Any){
}
}
I think the problem is:
Your collectionView dataSource is called only once. Since the image url loading is asynchronous, you will need to refresh your collectionview every time new data is appended to your datasource array like this:
self.posts.append(urlimage)
collectionView.reloadData()
or:
var posts = [UIImage](){
didSet{
collectionView.reloadData()
}
}
Hope this helps.
Edit update:
Regarding the asynchronous calls, i think you should use escaping closure that runs the code block once the network request receives a response.
First separate the network call functions like:
func fetchUsers(completion: #escaping(_ dictionary: [String: NSDictionary])->()){
let uid = Auth.auth().currentUser!.uid
let uids = Database.database().reference().child("users")
uids.observe(DataEventType.value, with: { (snapshot) in
let dict = snapshot.value as! [String:NSDictionary]
completion(dict)
})
}
func fetchURLS(completion: #escaping(_ dictionary: [String: String])->()){
let ref = Database.database().reference().child("posts")
ref.observe(DataEventType.value, with: { (snapshot2) in
let dict2 = snapshot2.value as! [String:String]
completionTwo(dict2)
})
}
Then, the parsing functions:
func parseUsers(dictionary: [String: NSDictionary]){
for (_,value) in dictionary {
if let uid = value["uid"] as? String{
self.following.append(uid)
}
}
fetchURLS { (urlDictionary) in
self.parseImageURLS(dictionary: urlDictionary)
}
}
func parseImageURLS(dictionary: [String: String]){
for(key, value) in dictionary{
for uid2 in self.following{
if (uid2 == key){
for (key2,value2) in value as! [String:String]{
//print(key2 + "this is key2")
if(key2 == "urlToImage"){
let urlimage = value2
//print(urlimage)
self.posts1.append(urlimage)
self.collectionview.reloadData()
print(self.posts1.count)
}
}
}
}
}
}
Then you add:
fetchUsers { (usersDictionary) in
self.parseUsers(dictionary: usersDictionary)
}
in viewDidLoad()
Hope this solves your problem. On a side note: I recommend using models and separating the network calls in a different file. Feel free to ask any questions.
I figured out how to do it after more searching.
I was incorrectly assuming that the CollectionView is loaded after the viewDidLoad() function is done. The helper classes for a CollectionView are called to a call of reloadData.
I observed that my reloadData call wasn't being called. In order to make this work, I add 2 lines of code to the viewDidLoad function:
collectionview.delegate = self
collectionview.dataSource = self
With this change, the images now load.
I created scheduleDict1 and inserted it into scheduleArray1. I can only get the closure to "see" scheduleArray1 when it is declared in the closure as in the code now. However, I can't access scheduleArray1 anywhere other than the closure.
I have tried declaring the scheduleArray1 in the MainViewController class instead of the closure but it will not be seen inside the closure!
import UIKit
import Firebase
class MainViewController: UITableViewController {
// var scheduleArray1 = [[String: String]]()
var scheduleDict = [String: Any]()
override func viewDidLoad() {
super.viewDidLoad()
retrieveSchedule()
}
func retrieveSchedule(){
let ref = Database.database().reference(withPath: "squadrons/vt-9/2019-04-02/events/")
ref.observe(.value) { (snapshot) in
var scheduleArray1 = [[String: String]]()
var count = 0
let enumerator = snapshot.children
while let rest = enumerator.nextObject() {
let refToPost = Database.database().reference(withPath: "squadrons/vt-9/2019-04-02/events/" + "\(count)")
refToPost.observe(.value, with: { (snapshot) in
// let data = snapshot.children
let scheduleDict1 = snapshot.value as! [String: String]
scheduleArray1.append(scheduleDict1)
// self.print(scheduleArray1)
})
count += 1
}
}
}
}
As the title says, I only want reloadtableview() to be called once. How do i approach this?
Here is my code:
var posts = [Post]() {
didSet {
posts.reverse()
self.tableView.reloadData()
}
}
func fetchData(){
currentQuery.observe(.value, with: { (snapshot) in
if let snapshot = snapshot.children.allObjects as? [DataSnapshot]{
var foundPosts = [Post]()
for snap in snapshot {
if let postDict = snap.value as? Dictionary<String, Any>{
let key = snap.key
let post = Post.init(postKey: key, postData: postDict)
foundPosts.append(post)
}
}
self.posts = foundPosts
geoQuery?.removeAllObservers()
}
})
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
fetchData()
// if I put "tableView.reloadData()" here, it wont load when the app opens for the first time
}
With this, the tableview is reloading everytime something is changed in the database. I want to only reload it once, but still have that observer there.
If I understood you right, the code below should do what you wanted. (Couldn't squeeze it into the comments)
var shouldReloadTableView = true
var posts = [Post]() {
didSet {
posts.reverse()
guard shouldReloadTableView else { return }
shouldReloadTableView = false
self.tableView.reloadData()
}
}
This is my code
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var ref: DatabaseReference?
var postData = [String]()
var databaseHandle: DatabaseHandle?
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
ref = Database.database().reference()
databaseHandle = ref?.child("Posts").observe(DataEventType.value, with: { (snapshot)in
let post = snapshot.value as? String
if let actualPost = post {
self.postData.append(actualPost)
self.tableView.reloadData()
}
})
}
}
I want my UITable to read any changes I make in my firebase console but with this code nothing shows up on the UITable. When I change it to DataEventType.childChanged, it works but it only adds new posts and not changes to previous post. What am I doing wrong? I just want all changes to show on the UITable that I make on Firebase.
Thanks!
Have been updating my project to the newest firebase code and am now running into an issue.
Yesterday my app was working fine w/ the updates I implemented and all of the sudden my tableView is no longer displaying data and it's not even printing the data in my console.
Here's my code:
import UIKit
import Firebase
import MGSwipeTableCell
class FeedVCViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
var posts = [Post]()
static var imageCache = NSCache()
var isPostHidden = false
var likeRef: FIRDatabaseReference!
var dislikeRef: FIRDatabaseReference!
var postRef: FIRDatabaseReference!
var postLiked = false
var postDisliked = false
private var _post: Post?
var post: Post? {
return _post
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 38, height: 38))
imageView.contentMode = .ScaleAspectFit
let image = UIImage(named: "unilogo2Whtie")
imageView.image = image
navigationItem.titleView = imageView
DataService.ds.REF_POSTS.observeEventType(.Value, withBlock: { snapshot in
print(snapshot.value)
self.posts = []
if let snapshots = snapshot.children.allObjects as? [FIRDataSnapshot] {
for snap in snapshots {
print("SNAP: \(snap)")
if let postDict = snap.value as? Dictionary<String, AnyObject> {
let key = snap.key
let post = Post(postKey: key, dictionary: postDict)
self.posts.append(post)
}
}
}
self.tableView.reloadData()
})
// Do any additional setup after loading the view.
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return posts.count
}
I must have done something wrong in the update I'm assuming, can anyone help?
I'm surprised you're not getting an error on the following lines:
var likeRef: FIRDatabaseReference!
var dislikeRef: FIRDatabaseReference!
var postRef: FIRDatabaseReference!
For me, I had to add the following line below import Firebase in order not to get an error when declaring the database reference:
import FirebaseDatabase
Then in your viewDidLoad method you need to add the reference to your database
likeRef = FIRDatabase.database().reference()
However, with this new version of Firebase, you don't need a separate reference for likeRef, dislikeRef and postRef. You can make just one main reference and then access the child of the reference for those details.