Access a UIView's properties on a background thread - ios

I am trying to have my CollectionView scroll it's first cell after the view appears, and then again whenever a button is pressed. The problem is that the collectionView hasn't generated all it's cells at any of the view lifecycle functions.
My solution is to beging a while loop on a background thread that checks to see if collectionView.visibleCells.count > 0, and when it is, return to the main thread and scroll to the first cell. However, I get an error, telling me that I shouldn't access visibleCells off the main thread, and the app chugs when I do.
How can I achieve this functionality on the main thread, or check the number of cells in the background thread?
private func scrollToFirst() {
DispatchQueue.global(qos: .background).async { [weak self] in
if (self != nil) {
while(self!.collectionView.visibleCells.count != 0) {
DispatchQueue.main.async { [weak self] in
self!.collectionView.scrollToItem(at: IndexPath(item: 0, section: 0), at: .centeredHorizontally, animated: true)
}
}
}
}
}

There is a delegate method willDisplay that gets called right before a collectionViewCell gets displayed. If you previously had no cells and this gets called, then you know you are about to go from zero to more than zero cells.

Yeah, don't do that. UIKit is not thread-safe, so the data structures of view objects may change out from under you when you try to view them from a background threads.
It seems like there should be a better way to deal with this than waiting for cells to appear.
If you can't figure out a cleaner way to do it, you could use a Timer object, which runs on the main thread. That code might look something like this:
private func scrollToFirst(afterDelay delay: Double = 0.2) {
_ = Timer.scheduledTimer(withTimeInterval: delay, repeats: false) {
timer, [weak self] in
guard strongSelf = self else {
return
}
if strongSelf.collectionView.visibleCells.count != 0 {
self!.collectionView.scrollToItem(at: IndexPath(item: 0, section: 0), at: .centeredHorizontally, animated: true)
}
}
That code would fire once, and scroll to the beginning of the collection view if there are once the timer fires.

Related

How can i know when default functions animations finish?

In my application, i listed delivered notifications as sorted by date in tableView. If user tap to notifications from device notification screen, app highlights row. But before user presses notification on device main screen, if users scroll towards the end of the tableView, when user presses notification, app scrolls the tableView to the row with as an animated and also highlights the row.
But if users scrolled contentView to end of the tableView, highlight is not working. I think scroll to row animation function and highlight animation function working at same time.
How can i catch when tableView.scrollToRow(at: indexPath, at: .bottom, animated: true) animation finished. If i know when scrollToRow animation finish, i can run highlight row code after.
Thanks.
DispatchQueue.main.async {
self.tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
if let cell = self.tableView.cellForRow(at: indexPath) as? NewsItemCell {
let highlightView = UIView.init(frame: cell.contentView.frame)
highlightView.backgroundColor = .white
highlightView.alpha = 0.0
cell.contentView.addSubview(highlightView)
print(indexPath)
UIView.animate(withDuration: 1.0, delay: 0, options: .curveEaseIn) {
highlightView.alpha = 1.0
} completion: { Bool in
UIView.animate(withDuration: 1.5, delay: 0, options: .curveEaseOut) {
highlightView.alpha = 0.0
} completion: { Bool in
highlightView.removeFromSuperview()
}
}
}
}
A UITableView is a subclass of UIScrollView.
You should be able to implement the UIScrollViewDelegate method scrollViewDidEndScrollingAnimation(_:) in your table view delegate (usually the owning view controller) and then highlight the row in your implementation of that method. (You'll probably need to add logic and state variables to track the fact that you are in this situation.)
Edit:
Note that as mentioned in the answer from #trndjc linked by HangerRash, you should your follow-on animation code from a call to Dispatch.main.async(). (That will help avoid stutters in the animations.)
From that answer:
Second, despite the fact that the method is called on the main thread, if you're running a subsequent animation here (one after the scroll has finished), it will still hitch (this may or may not be a bug in UIKit). Therefore, simply dispatch any follow-up animations back onto the main queue which just ensures that the animations will begin after the end of the current main task (which appears to include the scroll-to-row animation). Doing this will give you the appearance of a true completion.
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
DispatchQueue.main.async {
// execute subsequent animation here
}
}

Recursion without calling previous functions

How one can create a recursion function that once called will only execute the current called function that has the next index?
The collection view to be auto scrolled is nested inside the first table view cell.
I've been trying to create a recursion function that would work as a regular loop. The loops can not be used because it runs on a background thread.
This is my code:
var indexItem = 1
func autoScroll(time: Int) {
DispatchQueue.main.async {
self.run(after: time) {
cell._collectionView.scrollToItem(
at: IndexPath(row: indexItem, section: 0),
at: .centeredHorizontally, animated: true
)
indexItem += 1
autoScroll(time: 3)
return
}
}
}
autoScroll(time: 3)
The problem is that it always calls the function with the previous index first, then it executes the function with the actual index.
I believe what you're trying to do is this:
func autoScroll(time: DispatchTimeInterval, indexItem: Int = 1) {
DispatchQueue.main.asyncAfter(deadline: .now() + time) {
cell._collectionView.scrollToItem(at: IndexPath(row: indexItem, section: 0), at: .centeredHorizontally, animated: true)
autoScroll(time: time, indexItem: indexItem + 1)
}
}
autoScroll(time: .seconds(3))
Just pass the value to the function.
You really need some flag that will stop this, though. As written, this is ensuring that cell can never be released. And if multiple cells run this, then it's definitely going to cause a problem a they fight. I would expect this to run directly on the collection view rather than on the cell.

How to scroll to the last message sent when viewdidload? in swift 4

Currently, I have tried
func scrollToBottom(){
DispatchQueue.global(qos: .background).async {
let indexPath = IndexPath(item: self,messageArrat.count-1, section: 0)
self.messageTableView.scrollToRow(at: indexPath,at:.bottom,animated:true)
}
}
However it does not seem to work, outputting
Thread 16: ESC_BAD_ACCESS (code=1, address=0xfffffffffffffffc)
UITableView.scrollToRow(at:at:animated:) must be used from main thread only
I call the function right after setting delegate and datasource in viewdidload.
Sounds like a timing issue, you said you are calling this from the viewDidLoad, try moving it to the viewDidAppear or viewDidLayoutSubviews instead. Also remove from background thread as you are manipulating the UI and that needs to happen on the main thread.
First of all: please provide your code as text not as Image.
Now to your problem, you are trying to scroll to the last row in a global queue, but all UI-Updates must happen in the Main Thread, so simply remove the DispatchQueue, or if you are not in the main thread, you can write DispatchQueue.main instead of global
E.g.:
DispatchQueue.main.async {
//code Here
}
But i think you doesnt need the DispatchQueue at all so try to omit it completely.
Writing code in ".async" closure you starting a new thread, and the error message literally tells you do not do that.
Delete DispatchQueue stuff and that's it.
And please post text, description and all that stuff to help community.
You don't need to add DispatchQueue:
let indexPath = IndexPath(item: self.messageArrat.count-1, section: 0)
self.messageTableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
And if you want DispatchQueue then add main queue like that:
DispatchQueue.main.async {
let indexPath = IndexPath(item: self.messageArrat.count-1, section: 0)
self.messageTableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
May be index path must be out of bound, try checking number of rows before scrolling . Also do this all oprations on main thread.
It likes to work on this main thread and not in the background because it is related to the UI
try this code:
func scrollToBottom() {
DispatchQueue.main.async {
let indexPath = IndexPath(item: self.messageArrat.count-1, section: 0)
self.messageTableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
}

TableView Segue to new ViewController does not work twice

I have a TableViewController in which selecting a row makes a network call, shows an activityIndicator, and in the completion block of the network call, stops the indicator and does a push segue.
In testing, I found that when going to the new view controller, going back, and selecting another row, it shows the activity indicator, but never stops or calls the push segue. The code for didSelect:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
self.tableView.userInteractionEnabled = false
self.searchController?.active = false
self.idx = indexPath
let cell = tableView.cellForRowAtIndexPath(indexPath) as! CustomTableViewCell
cell.accessoryView = self.activityIndicator
self.activityIndicator.startAnimating()
self.networkingEngine?.getObjectById("\(objectId)", completion: { (object) -> Void in
if object != nil {
self.tableView.userInteractionEnabled = true
self.chosenObject = object
self.activityIndicator.stopAnimating()
self.tableView.reloadRowsAtIndexPaths([self.idx], withRowAnimation: .None)
self.tableView.deselectRowAtIndexPath(self.idx, animated: true)
self.performSegueWithIdentifier("segueToNewVC", sender: self)
}
else {
}
})
}
and my network call using Alamofire
func getObjectbyId(objectId: String, completion:(Object?) -> Void) {
Alamofire.request(Router.getObjectById(objectId: objectId))
.response { (request, response, data, error) -> Void in
if response?.statusCode != 200 {
completion(nil)
}
else if let parsedObject = try!NSJSONSerialization.JSONObjectWithData(data as! NSData, options: .MutableLeaves) as? NSDictionary {
let object = parsedObject["thing"]
dispatch_async(dispatch_get_main_queue(), { () -> Void in
completion(object)
})
}
}
}
So I made breakpoints, and it actually is going into the completion block the second time and calling it to stop indicator, reload that row, deselect the row, re-enable the tableView userInteraction, and perform the segue. It calls my prepareForSegue as well, but as soon as it finishes, it just sits with the indicator still spinning, and the RAM usage skyrockets up to almost 2GB until the simulator crashes.
I believe it has to do with multi-threading but I can't narrow down the exact issue since I'm putting my important stuff on the main thread.
The problem had to do with making the accessoryView of the cell an activityIndicator and then removing it and reloading it all while segueing away. I couldn't figure out how to keep it there and stop the multi-threading issue but I settled for a progress AlertView. If anybody has a solution, I'd love to know how this is fixed.

Dynamically scroll through tableView with scrollToRowAtIndexPath

I'd like to visually scroll through my whole tableView. I tried the following, but it doesn't seem to perform the scrolling. Instead it just runs through the loops. I inserted a dispatch_async(dispatch_get_main_queue()) statement, thinking that that would ensure the view is refreshed before proceeding, but no luck.
What am I doing wrong?
func scrollThroughTable() {
for sectionNum in 0..<tableView.numberOfSections() {
for rowNum in 0..<tableView.numberOfRowsInSection(sectionNum) {
let indexPath = NSIndexPath(forRow: rowNum, inSection: sectionNum)
var cellTemp = self.tableView.cellForRowAtIndexPath(indexPath)
if cellTemp == nil {
dispatch_async(dispatch_get_main_queue()) {
self.tableView.scrollToRowAtIndexPath(indexPath!, atScrollPosition: .Top, animated: true)
self.tableView.reloadData()
}
}
}
}
}
I found a solution. Simply use scrollToRowAtIndexPath() with animation. To do so I had to create a getIndexPath() function to figure out where I want to scroll. Has more or less the same effect as scrolling through the whole table if I pass it the last element of my tableView.
If you want it to happen slower with more scrolling effect, wrap it inside UIView.animateWithDuration() and play with 'duration'. You can even do more animation if you want in its completion block. (No need to set an unreliable sleep timer, etc.)
func animateReminderInserted(toDoItem: ReminderWrapper) {
if let definiteIndexPath = indexPathDelegate.getIndexPath(toDoItem) {
self.tableView.scrollToRowAtIndexPath(definiteIndexPath, atScrollPosition: .Middle, animated: true)
}
}

Resources