How to update chat view smoothly when I get new message - ios

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)
}
}

Related

Execution order of closures

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.

Why RxSwift Subscribe just run once in First launch viewWillAppear?

I write a subscribe in viewWillAppear.
But it also run once in first launch app.
When I push to another viewcontroller, I use dispose().
Then I back in first viewcontroller, my subscribe func in viewWillAppear don't run.
What's wrong with my rx subscribe?
var listSubscribe:Disposable?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
listSubscribe = chatrooms.notifySubject.subscribe({ json in
print("*1") //just print once in first launch
self.loadContents()
})
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
let controllers = tabBarController?.navigationController?.viewControllers
if (controllers?.count)! > 1 {
listSubscribe?.dispose()
}
}
RxSwift documentation says "Note that you usually do not want to manually call dispose; this is only an educational example. Calling dispose manually is usually a bad code smell."
Normally, you should be doing something like this -
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
whatever.subscribe(onNext: { event in
// do stuff
}).disposed(by: self.disposeBag)
}
As for your question, I believe you don't need to re-subscribe because you subscription will be alive and 'notifySubject' will send you updates whenever there are any.
Maybe you can get some reactive implementation of viewWillAppear and similar functions? And forget about manual disposables handling... For example your UIViewController init will contain something like this:
rx.driverViewState()
.asObservable()
.filter({ $0 == .willAppear })
.take(1) // if you need only first viewWillAppear call
.flatMapLatest({ _ in
// Do what you need
})
And the implementation of driverViewState:
public extension UIViewController {
public enum ViewState {
case unknown, didAppear, didDisappear, willAppear, willDisappear
}
}
public extension Reactive where Base: UIViewController {
private typealias _StateSelector = (Selector, UIViewController.ViewState)
private typealias _State = UIViewController.ViewState
private func observableAppearance(_ selector: Selector, state: _State) -> Observable<UIViewController.ViewState> {
return (base as UIViewController).rx
.methodInvoked(selector)
.map { _ in state }
}
func driverViewState() -> Driver<UIViewController.ViewState> {
let statesAndSelectors: [_StateSelector] = [
(#selector(UIViewController.viewDidAppear(_:)), .didAppear),
(#selector(UIViewController.viewDidDisappear(_:)), .didDisappear),
(#selector(UIViewController.viewWillAppear(_:)), .willAppear),
(#selector(UIViewController.viewWillDisappear(_:)), .willDisappear)
]
let observables = statesAndSelectors
.map({ observableAppearance($0.0, state: $0.1) })
return Observable
.from(observables)
.merge()
.asDriver(onErrorJustReturn: UIViewController.ViewState.unknown)
.startWith(UIViewController.ViewState.unknown)
.distinctUntilChanged()
}
}

Catch event in view from another class

I have async task with request where i fetching products every 3 seconds in class Item.
class Item: NSManagedObject {
var is_fetching:Bool = false;
func fetchProducts(q: String) {
let task = session.dataTaskWithRequest(urlRequest, completionHandler: {
(data, response, error) in
self.is_fetching = true;
//some code
if ((response as! NSHTTPURLResponse).statusCode == 202) {
sleep(3)
self.fetchProducts(q)
return
}
if ((response as! NSHTTPURLResponse).statusCode == 200) {
self.is_fetching = false;
}
})
task.resume()
}
}
And i have UITableViewController where i show data from response. How do i update my cells when status code is 200:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath:
indexPath) as! CartTableViewCell
if item.is_fetching {
cell.fetchIndicator.startAnimating();
} else {
cell.fetchIndicator.stopAnimating();
cell.fetchIndicator.hidden = true;
}
}
You can do it in few ways.
NSNotificationCenter (simplest).
You can post notifications, that will trigger your controller's methods. Looks like this:
// if response.code == 200
NSNotificationCenter.defaultCenter().postNotificationName("kSomeConstatnString", object: nil)
...
// in viewDidLoad of your controller:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "updateTable", object: nil)
// you also need implement updateTable() func inside your controller
// or if you need just update table
NSNotificationCenter.defaultCenter().addObserver(self.tableView, selector: "reloadData", object: nil)
// do not forget to delete observer (for instance in -deinit method)
NSNotificationCenter.defaultCenter().removeObserver(self)
// or tableView. also you can specify for which selector, if you use couple of them.
Delegate pattern.
You can describe your protocol, make your controller implement this protocol and save it as instance in your model object. Then just call methods from delegate. Details here.
Block callbacks.
Create block for action and call it from your model. For example:
// inside controller
model.refreshCallback = { Void in
self.tableView.reloadData() // or whatever
}
// inside model
var refreshCallback: (() -> Void)?
...
// if result.code == 200
if let callback = refreshCallback {
callback()
}
Use one of the UITableView’s reload functions, perhaps:
func reloadRowsAtIndexPaths(_ indexPaths: [NSIndexPath],
withRowAnimation animation: UITableViewRowAnimation)
This will cause it to ask again for the cell in question. Make sure you do this on the main thread.

Pull to Refresh: data refresh is delayed

I've got Pull to Refresh working great, except when the table reloads there is a split second delay before the data in the table reloads.
Do I just have some small thing out of place? Any ideas?
viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
self.refreshControl?.addTarget(self, action: "handleRefresh:", forControlEvents: UIControlEvents.ValueChanged)
self.getCloudKit()
}
handleRefresh for Pull to Refresh:
func handleRefresh(refreshControl: UIRefreshControl) {
self.objects.removeAll()
self.getCloudKit()
dispatch_async(dispatch_get_main_queue(), { () -> Void in
refreshControl.endRefreshing()
})
}
Need the data in two places, so created a function for it getCloudKit:
func getCloudKit() {
publicData.performQuery(query, inZoneWithID: nil) { results, error in
if error == nil { // There is no error
for play in results! {
let newPlay = Play()
newPlay.color = play["Color"] as! String
self.objects.append(newPlay)
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
} else {
print(error)
}
}
}
tableView:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("reuseIdentifier", forIndexPath: indexPath)
let object = objects[indexPath.row]
if let label = cell.textLabel{
label.text = object.matchup
}
return cell
}
This is how you should do this:
In your handleRefresh function, add a bool to track the refresh operation in process - say isLoading.
In your getCloudKit function just before reloading the table view call endRefreshing function if isLoading was true.
Reset isLoading to false.
Importantly - Do not remove your model data before refresh operation is even instantiated. What if there is error in fetching the data? Delete it only after you get response back in getCloudKit function.
Also, as a side note, if I would you, I would implement a timestamp based approach where I would pass my last service data timestamp (time at which last update was taken from server) to server and server side would return me complete data only there were changes post that timestamp else I would expect them to tell me no change. In such a case I would simple call endRefreshing function and would not reload data on table. Trust me - this saves a lot and gives a good end user experience as most of time there is no change in data!

when to reloadData() on UITableView

Problem: When using Alamofire and SwiftyJSON to populate a UITableView, the table view loads but there is a 1 second pause before the data is displayed. I am not sure where I should be calling reloadData() to fix this.
There are various questions about when to call reloadData() using Alamofire and SwiftyJSON to populate a UITableView, but I have yet to find an answer that solves my problem.
A bit of background:
I am using the Google Places Web API to populate a UITableView with Google Place names, addresses and icons. Originally I was only using SwiftyJSON to accomplish this, but it was taking quite some time for the UITableView to load. Here is some of that code:
GooglePlacesRequest.swift:
...
var placesNearbyArray: [GooglePlaceNearby]?
if let url = NSURL(string: urlString) {
if let data = NSData(contentsOfURL: url, options: .allZeros, error: nil) {
let json = JSON(data: data)
placesNearbyArray = parseNearbyJSON(json)
completion(placesNearbyArray)
} else {
completion(placesNearbyArray)
}
} else {
completion(placesNearbyArray)
}
}
func parseNearbyJSON(json: JSON) -> [GooglePlaceNearby] {
var placesNearbyArray = [GooglePlaceNearby]()
for result in json["results"].arrayValue {
let name = result["name"].stringValue
let address = result["vicinity"].stringValue
...
placesNearbyArray.append(place)
}
return placesNearbyArray
}
And the code from viewWillAppear in the UITableView:
override func viewWillAppear(animated: Bool) {
...
placesRequest.getPlacesNearUserLocation(location, completion: { (googlePlaces) -> Void in
if let googlePlaces = googlePlaces {
self.venues = googlePlaces
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
})
}
As I said above, this approach was working, but the UITableView would often take 3 - 4 seconds to load. I then decided to use Alamofire along with SwiftyJSON and (posssibly) a cache.
Here are some snippets of the current code:
GooglePlacesRequest.swift:
...
var placesNearbyArray: [GooglePlaceNearby]?
request(.GET, urlString).responseSwiftyJSON({ (_, _, json, error) in
var innerPlacesArray = [GooglePlaceNearby]()
for result in json["results"].arrayValue {
let name = result["name"].stringValue
let address = result["vicinity"].stringValue
...
innerPlacesArray.append(place)
}
placesNearbyArray = innerPlacesArray
completion(placesNearbyArray)
})
completion(placesNearbyArray)
}
After adding this, I tried to use the same code from viewWillAppear in the UITableView, but this is what happens:
The UITableView loads much faster
There is then a 1 second pause before the table view cells are populated.
I have tried to place the new request function in various places in the UITableView, such as in the viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
...
let placesRequest = PlacesRequest()
placesRequest.fetchNearbyPlaces(location, completion: { (googlePlaces) -> Void in
if let googlePlaces = googlePlaces {
self.venues = googlePlaces
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
}
)
And I have also tried to call reloadData() on the main thread in both viewWillLoad and viewWillAppear.
The result is always the same...there is a 1 second pause before the UITableView loads the data.
Before I even implement my cache, I need to figure out how (and where) to properly make my request. Can anyone assist me with this?

Resources