Execution order of closures - ios

Here's my code:
I'm just a beginner in programming with this language and I have a problem with a dynamic collection view
My problem is that the the first print is executed after the second one and I'm wondering why...
class ViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
#IBOutlet weak var menuButton: UIBarButtonItem!
let db = Firestore.firestore()
let cellId: String = "cellId"
var news = [[String: Any]]()
override func viewDidLoad(){
super.viewDidLoad()
navigationItem.title = "Novità"
sideMenu()
customizeNavBar()
collectionView?.register(NovitaCellView.self, forCellWithReuseIdentifier: cellId)
}
override func didReceiveMemoryWarning(){
super.didReceiveMemoryWarning()
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
db.collection("News").getDocuments(){(querySnapshot, err) in
if let err = err{
print("Errore: \(err)")
}else{
for document in (querySnapshot?.documents)!{
let cell = document.data()
self.news.append(cell)
print ("document number: \(self.news.count)")
}
}
}
print("exe return with value: \(self.news.count)")
return self.news.count
}
edit: I tried setting it into the viewDidLoad func as well as setting it both as a sync queue and an async queue and it either doesn't works.
edit 2: i made this working by adding the closure into a max priority queue and reloading the view after in the main thread but it takes a long time in order to work..

The idea is that this line
db.collection("News").getDocuments(){(querySnapshot, err) in
is asynchronous (runs in another queue other than the main queue ) it's a network request that will run after the below line (which runs in main queue)

You are going to background thread mode when you call getDocuments which have less priority than Main thread.So replace below part of code into viewDidLoad or a method which is called from viewDidLoad
db.collection("News").getDocuments(){(querySnapshot, err) in
if let err = err{
print("Errore: \(err)")
}else{
for document in (querySnapshot?.documents)!{
let cell = document.data()
self.news.append(cell)
print ("document number: \(self.news.count)")
}
DispatchQueue.main.async {
tableview.reloadData()
}
}
}
and when you get your data you go back to main thread and reload your tableview using this code.
DispatchQueue.main.async {
tableView.reloadData()
}
You can learn more about threading from apple documentation and this tutorial also.

Related

Am I implementing the tableviewdatasource correctly to get downloaded data to show?

I am developing a small app to connect to my site, download data via a PHP web service, and display it in a table view. To get started I was following a tutorial over on Medium by Jose Ortiz Costa (Article on Medium).
I tweaked his project and got it running to verify the Web service was working and able to get the data. Once I got that working, I started a new project and tried to pull in some of the code that I needed to do the networking and tried to get it to display in a tableview in the same scene instead of a popup scene like Jose's project.
This is where I am running into some issues, as I'm still rather new to the swift programming language (started a Udemy course and have been picking things up from that) getting it to display in the table view. I can see that the request is still being sent/received, but I cannot get it to appear in the table view (either using my custom XIB or a programmatically created cell). I thought I understood how the code was broken down, and even tried to convert it from a UITableViewController to a UITableviewDataSource via an extension of the Viewcontroller.
At this point, I'm pretty stumped and will continue to inspect the code and tweak what I think might be the root cause. Any pointers on how to fix would be really appreciated!
Main Storyboard Screenshot
Struct for decoding my data / Lead class:
import Foundation
struct Lead: Decodable {
var id: Int
var name: String
var program: String
var stage: String
var lastAction: String
}
class LeadModel {
weak var delegate: Downloadable?
let networkModel = Network()
func downloadLeads(parameters: [String: Any], url: String) {
let request = networkModel.request(parameters: parameters, url: url)
networkModel.response(request: request) { (data) in
let model = try! JSONDecoder().decode([Lead]?.self, from: data) as [Lead]?
self.delegate?.didReceiveData(data: model! as [Lead])
}
}
}
ViewController:
import UIKit
class LeadViewController: UIViewController {
// Buttons
#IBOutlet weak var newButton: UIButton!
#IBOutlet weak var firstContactButton: UIButton!
#IBOutlet weak var secondContactButton: UIButton!
#IBOutlet weak var leadTable: UITableView!
let model = LeadModel()
var models: [Lead]?
override func viewDidLoad() {
super.viewDidLoad()
//Make Buttons rounded
newButton.layer.cornerRadius = 10.0
firstContactButton.layer.cornerRadius = 10.0
secondContactButton.layer.cornerRadius = 10.0
//Delegate
model.delegate = self
}
//Send request to web service based off Buttons Name
#IBAction func findLeads(_ sender: UIButton) {
let new = sender.titleLabel?.text
let param = ["stage": new!]
print ("findLead hit")
model.downloadLeads(parameters: param, url: URLServices.leads)
}
}
extension LeadViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
print ("number of sections hit")
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
guard let _ = self.models else {
return 0
}
print ("tableView 1 hit")
return self.models!.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Create an object from LeadCell
let cell = tableView.dequeueReusableCell(withIdentifier: "leadID", for: indexPath) as! LeadCell
// Lead selection
cell.leadName.text = self.models![indexPath.row].name
cell.actionName.text = self.models![indexPath.row].lastAction
cell.stageName.text = self.models![indexPath.row].stage
cell.progName.text = self.models![indexPath.row].program
print ("tableView 2 hit")
// Return the configured cell
return cell
}
}
extension LeadViewController: Downloadable {
func didReceiveData(data: Any) {
//Assign the data and refresh the table's data
DispatchQueue.main.async {
self.models = data as? [Lead]
self.leadTable.reloadData()
print ("LeadViewController Downloadable Hit")
}
}
}
EDIT
So with a little searching around (okay...A LOT of searching around), I finally found a piece that said I had to set the class as the datasource.
leadTable.dataSource = self
So that ended up working (well after I added a prototype cell with the identifier used in my code). I have a custom XIB that isn't working right now and that's my next tackle point.
You load the data, but don't use it. First, add the following statement to the end of the viewDidLoad method
model.delegate = self
Then add the following LeadViewController extension
extension LeadViewController: Downloadable {
func dicReceiveData(data: [Lead]) {
DispatchQueue.main.async {
self.models = data
self.tableView.reloadData()
}
}
}
And a couple of suggestions:
It is not a good practice to use the button title as a network request parameter:
let new = sender.titleLabel?.text
let param = ["stage": new!]
It is better to separate UI and logic. You can use the tag attribute for buttons (you can configure it in the storyboard or programmatically) to check what button is tapped.
You also have several unnecessary type casts in the LeadModel class. You can change
let model = try! JSONDecoder().decode([Lead]?.self, from: data) as [Lead]?
self.delegate?.didReceiveData(data: model! as [Lead])
to
do {
let model = try JSONDecoder().decode([Lead].self, from: data)
self.delegate?.didReceiveData(data: model)
}
catch {}

How to display tableView only after data fetched from web? (Swift)

I encountered difficulties when loading the Collection views nested in table view cells. The content inside cells would only show after scrolling the table a couple of times. My approach was to use DispatchGroup() in order to fetch the data in a background thread but it didn't work. What is there to do in order to show all the information at once without scrolling through table?
ViewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
_tableView.isHidden = true
_tableView.dataSource = nil
_tableView.delegate = nil
SVProgressHUD.show()
view.backgroundColor = UIColor.flatBlack()
getData()
dispatchGroup.notify(queue: .main) {
SVProgressHUD.dismiss()
self._tableView.isHidden = false
self._tableView.dataSource = self
self._tableView.delegate = self
self._tableView.reloadData()
}
}
UICollectionView and UITableView datasource / OtherMethods
func getData(){
dispatchGroup.enter()
backend.movieDelegate = self
backend.actorDelegate = self
backend.getMoviePopularList()
backend.getMovieTopRatedList()
backend.getMovieUpcomingList()
backend.getPopularActors()
backend.getMovieNowPlayingList()
dispatchGroup.leave()
}
func transferMovies(data: [String:[MovieModel]]) {
dispatchGroup.enter()
popularMovies = data
dispatchGroup.leave()
}
func transferActors(data: [ActorModel]) {
dispatchGroup.enter()
popularActors = data
dispatchGroup.leave()
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "DiscoverCell") as? DiscoverViewCell else { return UITableViewCell()}
cell.categoryLabel.text = cell.categories[indexPath.item]
//categories[indexPath.item]
cell._collectionView.delegate = self
cell._collectionView.dataSource = self
cell._collectionView.tag = indexPath.row
cell._collectionView.reloadData()
self.setUpCell(cell)
return cell
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MovieCell", for: indexPath) as? MovieCollectionViewCell else { return UICollectionViewCell()}
if collectionView.tag == 0{
if let movieDetails = popularMovies["Popular"]?[indexPath.item] {
cell.updateMovieCollectionCell(movie: movieDetails)
}
} else if collectionView.tag == 1{
if let movieDetails = popularMovies["Top rated"]?[indexPath.item] {
cell.updateMovieCollectionCell(movie: movieDetails)
}
} else if collectionView.tag == 2{
if let movieDetails = popularMovies["Upcoming"]?[indexPath.item] {
cell.updateMovieCollectionCell(movie: movieDetails)
} else if collectionView.tag == 3{
cell.movieTitleLabel.text = popularActors?[indexPath.item].name ?? ""
cell.moviePicture.image = popularActors?[indexPath.item].poster
}
} else if collectionView.tag == 4{
if let movieDetails = popularMovies["Now playing"]?[indexPath.item] {
cell.updateMovieCollectionCell(movie: movieDetails)
}
}
return cell
}
MovieCollectionViewCell
class MovieCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var moviePicture: UIImageView!
#IBOutlet weak var movieTitleLabel: UILabel!
func updateMovieCollectionCell(movie: MovieModel){
moviePicture.image = movie.poster
movieTitleLabel.text = movie.name
}
}
DiscoverViewCell
class DiscoverViewCell: UITableViewCell {
#IBOutlet weak var categoryLabel: UILabel!
#IBOutlet weak var _collectionView: UICollectionView!
let categories = ["Popular", "Top Rated", "Upcoming", "Popular People", "Now playing"]
#IBAction func seeMoreAction(_ sender: Any) {
}
}
My intention is to show a loading animation until all the data is fetched and the display the table view cells containing the collection views with fetched data from web.
The desired result should look like this when opening the app
From what I can tell, you're using dispatchGroup incorrectly.
To summarize notify():
it runs the associated block when all currently queued blocks in the group are complete
the block is only run once and then released
if the group's queue is empty, the block is run immediately
The issue I see is that with the way your fetch code is written, the group thinks its queue is empty when you call notify. So the notify() block is run immediately and then you only see the cells populate when they are reloaded during scrolling.
There are two ways to populate a dispatchGroup's queue:
call DispatchQueue.async() and pass the group to directly enqueue a block and associate it with the group
manually call enter() when a block begins and leave() when it ends, which increases/decreases an internal counter on the group
The first way is safer, since you don't have to keep track of the blocks yourself, but the second one is more flexible if you don't have control over what queue a block is run on for example.
Since you're using enter/leave, you need to make sure that you call enter() for each separate work item (in your case, the asynchronous calls to backend), and only call leave() when each one those work items completes. I'm not sure how you're using the delegate methods, but there doesn't seem to one for each backend call, since there are 5 different calls and only 2 delegate methods. It also doesn't look like the delegate methods would be called if an error happened in the backend call.
I would recommend changing the backend calls to use completion blocks instead, but if you want to stick with the delegate pattern, here's how you might do it:
func getData(){
backend.movieDelegate = self
backend.actorDelegate = self
dispatchGroup.enter()
backend.getMoviePopularList()
dispatchGroup.enter()
backend.getMovieTopRatedList()
dispatchGroup.enter()
backend.getMovieUpcomingList()
dispatchGroup.enter()
backend.getPopularActors()
dispatchGroup.enter()
backend.getMovieNowPlayingList()
dispatchGroup.notify(queue: .main) {
SVProgressHUD.dismiss()
self._tableView.isHidden = false
self._tableView.dataSource = self
self._tableView.delegate = self
self._tableView.reloadData()
}
}
func transferPopularMovies(data: [MovieModel]) {
popularMovies = data
dispatchGroup.leave()
}
func transferTopRatedMovies(data: [MovieModel]) {
topRatedMovies = data
dispatchGroup.leave()
}
func transferUpcomingMovies(data: [MovieModel]) {
upcomingMovies = data
dispatchGroup.leave()
}
func transferActors(data: [ActorModel]) {
popularActors = data
dispatchGroup.leave()
}
func transferNowPlayingMovies(data: [MovieModel]) {
nowPlayingMovies = data
dispatchGroup.leave()
}
Don't forget to call the delegate methods even if there is an error to make sure the enter/leave calls are balanced. If you call enter more often than leave, the notify block never runs. If you call leave more often, you crash.
Try this...After getting your data from backend and assigning to moviedetails, set your delegate and datasource to self and reload table.
Set this line into the bottom of getData() function and run
self._tableView.isHidden = false
self._tableView.dataSource = self
self._tableView.delegate = self
self._tableView.reloadData()
and remove from viewdidload()

JSON data shown on viewDidAppear and not viewDidLoad

I have this that download the JSON and I have appended them into an array
func downloadDetails(completed: #escaping DownloadComplete){
Alamofire.request(API_URL).responseJSON { (response) in
let result = response.result
let json = JSON(result.value!).array
let jsonCount = json?.count
let count = jsonCount!
for i in 0..<(count){
let image = json![i]["urls"]["raw"].stringValue
self.imagesArray.append(image)
}
self.tableView.reloadData()
print(self.imagesArray)
completed()
}
}
I know that viewDidLoad loads first and viewDidAppear load later. I am trying to retrieve the array that contain all the informations but it's not showing on viewDidLoad as the array is empty but its showing on viewDidAppear with an array of ten and I dont know how to get that from viewDidAppear.
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var tableView: UITableView!
var imageList: Image!
var imagesArray = [String]()
override func viewDidLoad() {
super.viewDidLoad()
print("viewDidLoad " ,imagesArray)
setupDelegate()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
downloadDetails {
DispatchQueue.main.async {
}
print("viewDidAppear ", self.imagesArray)
}
}
The output is as follow
viewDidLoad []
viewDidAppear ["https://images.unsplash.com/photo-1464550883968-cec281c19761", "https://images.unsplash.com/photo-1464550838636-1a3496df938b", "https://images.unsplash.com/photo-1464550580740-b3f73fd373cb", "https://images.unsplash.com/photo-1464547323744-4edd0cd0c746", "https://images.unsplash.com/photo-1464545022782-925ec69295ef", "https://images.unsplash.com/photo-1464537356976-89e35dfa63ee", "https://images.unsplash.com/photo-1464536564416-b73260a9532b", "https://images.unsplash.com/photo-1464536194743-0c49f0766ef6", "https://images.unsplash.com/photo-1464519586905-a8c004d307cc", "https://images.unsplash.com/photo-1464519046765-f6d70b82a0df"]
What is the right way to access the array with information in it?
that's because the request is asynchronous it's not getting executed as serial lines of code , there should be a delay according to network status , besides you don't even call downloadDetails inside viewDidLoad
//
you also need to reload table here
downloadDetails { // Alamofire callback by default runs in main queue
self.tableView.reloadData()
}
Just call your method on
viewDidLoad()
And maybe you should learn more about iOS lifecycle, so you can spot the difference between viewDidLoad and viewDidAppear 😉

How to update chat view smoothly when I get new message

I have a question about my chat app.
And I have a view about chatting to others.
chat view like this.
But when others send message to me, I should update my view to let user knows new message text,I also save message in my sqlite.
And I update view in main thread when user gets message.
It makes view a while locked, and I want to send message to my chat target,I should wait the view updating to end.
In general, How to deal with updating chat view?
Thanks.
import UIKit
import RxCocoa
import RxSwift
class ViewController: UIViewController {
var subscribe:Disposable?
var texts:[Message] = [Message]()
override func viewDidLoad() {
super.viewDidLoad()
self.subscribe = chatroom.PublishSubject<Any>().subscribe({ json in
DispatchQueue.main.async {
self.loadTexts()
}
})
}
func loadTexts() {
self.texts.removeAll(keepingCapacity: false)
self.chatroom.messages.forEach({ (id,message) in
self.texts.append(message)
})
self.texts.sort(by: { (a,b) in
a.time < b.time
})
self.tableView.reloadData()
//updateViewFrame()
if self.texts.count > 0 {
self.tableView.scrollToRow(at: IndexPath.init(row: self.texts.count-1,section: 0), at: .bottom, animated: false)
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return texts.count
}
}
All UI-related work must always be run in the main thread.
DispatchQueue.global().async() {
print("Work Dispatched")
// Do heavy or time consuming work
// Then return the work on the main thread and update the UI
DispatchQueue.main.async() {
// Return data and update on the main thread, all UI calls should be on the main thread
self.tableView.reloadData()
}
}
DispatchQueue.global(qos: .userInitiated).async {
DispatchQueue.main.async {
self.chatTableView.dataSource = self
self.chatTableView.delegate = self
self.chatTableView.reloadData()
self.tableViewScrollToBottom(animated: false)
}
}

How to correctly load information from Firebase?

I will try my best to explain what I'm doing. I have an infinite scrolling collection view that loads a value from my firebase database for every cell. Every time the collection view creates a cell it calls .observe on the database location and gets a snapchat. If it loads any value it sets the cells background to black. Black background = cell loaded from database. If you look at the image below you can tell not all cells are loading from the database. Can the database not handle this many calls? Is it a threading issue? Does what I'm doing work with firebase? As of right now I call firebase in my
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
method
After some testing it seems that it is loading everything from firebase fine, but it's not updating the UI. This leads me to believe it's creating the cell before it's loading the information for some reason? I will try to figure out how to make it 'block' somehow.
The image
You should delegate the loading to the cell itself, not your collectionView: cellForItemAtIndexPath method. The reason for this is the delegate method will hang asynchronously and for the callback of the FireBase network task. While the latter is usually quick (by experience), you might have some issues here with UI loading.. Judging by the number of squares on your view..
So ideally, you'd want something like this:
import FirebaseDatabase
class FirebaseNode {
//This allows you to set a single point of reference for Firebase Database accross your app
static let ref = Database.database().reference(fromURL: "Your Firebase URL")
}
class BasicCell : UICollectionViewCell {
var firPathObserver : String { //This will make sure that as soon as you set the value, it will fetch from firebase
didSet {
let path = firPathObserver
FirebaseNode.ref.thePathToYouDoc(path) ..... {
snapshot _
self.handleSnapshot(snapshot)
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setupSubViews()
}
func setupSubViews() {
//you add your views here..
}
func handleSnapshot(_ snapshot: FIRSnapshot) {
//use the content of the observed value
DispatchQueue.main.async {
//handle UI updates/animations here
}
}
}
And you'd use it:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let path = someWhereThatStoresThePath(indexPath.item)//you get your observer ID according to indexPath.item.. in whichever way you do this
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Your Cell ID", for: indexPath) as! BasicCell
cell.firPathObserver = path
return cell
}
If this doesn't work, it's probable that you might be encountering some Firebase limitation.. which is rather unlikely imo.
Update .. with some corrections and with local cache.
class FirebaseNode {
//This allows you to set a single point of reference for Firebase Database accross your app
static let node = FirebaseNode()
let ref = Database.database().reference(fromURL: "Your Firebase URL")
//This is the cache, set to private, since type casting between String and NSString would add messiness to your code
private var cache2 = NSCache<NSString, DataSnapshot>()
func getSnapshotWith(_ id: String) -> DataSnapshot? {
let identifier = id as NSString
return cache2.object(forKey: identifier)
}
func addSnapToCache(_ id: String,_ snapshot: DataSnapshot) {
cache2.setObject(snapshot, forKey: id as NSString)
}
}
class BasicCell : UICollectionViewCell {
var firPathObserver : String? { //This will make sure that as soon as you set the value, it will fetch from firebase
didSet {
handleFirebaseContent(self.firPathObserver)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setupSubViews()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupSubViews() {
//you add your views here..
}
func handleFirebaseContent(_ atPath: String?) {
guard let path = atPath else {
//there is no content
handleNoPath()
return
}
if let localSnap = FirebaseNode.node.getSnapshotWith(path) {
handleSnapshot(localSnap)
return
}
makeFirebaseNetworkTaskFor(path)
}
func handleSnapshot(_ snapshot: DataSnapshot) {
//use the content of the observed value, create and apply vars
DispatchQueue.main.async {
//handle UI updates/animations here
}
}
private func handleNoPath() {
//make the change.
}
private func makeFirebaseNetworkTaskFor(_ id: String) {
FirebaseNode.node.ref.child("go all the way to your object tree...").child(id).observeSingleEvent(of: .value, with: {
(snapshot) in
//Add the conditional logic here..
//If snapshot != "<null>"
FirebaseNode.node.addSnapToCache(id, snapshot)
self.handleSnapshot(snapshot)
//If snapshot == "<null>"
return
}, withCancel: nil)
}
}
One point however, using NSCache: this works really well for small to medium sized lists or ranges of content; but it has a memory management feature which can de-alocated content if memory becomes scarce.. So when dealing with larger sets like yours, you might as well go for using a classing Dictionnary, as it's memory will not be de-alocated automatically. Using this just becomes as simple as swaping things out:
class FirebaseNode {
//This allows you to set a single point of reference for Firebase Database accross your app
static let node = FirebaseNode()
let ref = Database.database().reference(fromURL: "Your Firebase URL")
//This is the cache, set to private, since type casting between String and NSString would add messiness to your code
private var cache2 : [String:DataSnapshot] = [:]
func getSnapshotWith(_ id: String) -> DataSnapshot? {
return cache2[id]
}
func addSnapToCache(_ id: String,_ snapshot: DataSnapshot) {
cache2[id] = snapshot
}
}
Also, always make sure you got through the node strong reference to Firebasenode, this ensures that you're always using the ONE instance of Firebasenode. Ie, this is ok: Firebasenode.node.ref or Firebasenode.node.getSnapshot.., this is not: Firebasenode.ref or Firebasenode().ref

Resources