Programmatic .beginRefresh() not refreshing data in UITableView - ios

In my TableViewController, I have an #objc function that fetches data using Alamofire. The function is called by the refreshController when the user pulls down to trigger a pull refresh.
The pull refresh is declared like so:
let refreshControl = UIRefreshControl()
tableView.refreshControl = refreshControl
self.refreshControl!.addTarget(self, action: #selector(refresh(sender:)), for: .valueChanged)
And the function it runs:
#objc func refresh(sender:AnyObject) {
let defaults = UserDefaults.standard
if let mode = defaults.string(forKey: defaultsKeys.mode) {
self.mode = mode
}
self.tableData.removeAll()
print(mode)
Alamofire.request(mode).response { response in
let xml = SWXMLHash.config {
config in
config.shouldProcessLazily = true
}.parse(response.data!)
for elem in xml["rss"]["channel"]["item"].all {
self.tableData.append(elem["title"].element!.text)
}
print("Completed Data Gather")
self.mainTable?.reloadData()
self.refreshControl!.endRefreshing()
}
}
The issue comes when I attempt to call the pull refresh function programmatically using
self.refreshControl!.beginRefreshing()
self.refreshControl!.sendActions(for: .valueChanged)
The function executes (as observed in the console due to the print statements) and the data is fetched, but the tableView refuses to refresh. I know the data is fetched properly because the next time the user manually pull refreshes the tableView updates with the new data.
TL;DR My tableView properly reloads when the user manually pull refreshes, but not when the pull refresh is called programmatically using.

As Paul mentioned in the comments, this is because UI updates must be dispatched on the main thread.
Alamofire is asynchronous and runs on a background thread. With that being said, all of the code within your Alamofire request will execute on the background thread.
In order to execute code on the main thread, utilize the following:
DispatchQueue.main.async {
// Update UI ie. self.mainTable?.reloadData()
}

Related

skipPrevious and skipNext buttons inactive even with items in queue Google Cast iOS Sender SDK v4.3.5 and above

I have an iOS sender application for video streaming that supports queueing and using the skipPrevious and skipNext buttons to skip forward and backward between videos in the queue. The app works with the google cast sdk v4.3.3 and v4.3.4 but I need to update the sdk to support iOS 13 changes. When I updated the sdk to v4.4.4 the skipPrevious and skipNext button types on the ExpandedMediaControlsViewController always appear greyed out even when I can see both on the receiver and by printing in the sender app that there are items in the queue. The buttons appear greyed out in all versions of the sdk v4.3.5 and later.
I have looked at the Google Chromecast developer documentation and the skipPrevious and skipNext button types are not deprecated and say that they should update automatically if there is something in the queue. I tried modifying google's iOS sender app tutorial code to change the 30 second ffw and rwd buttons to the skip buttons and had the same results after adding items to the queue and playing.
There is another unanswered question about a similar issue that was created in March here: skipNext skipPrevious Google Cast greyed out
I am using an update function inside of my castViewController class to change the expandedMediaControls to the skipPrevious and skipNext types. I call this method when my castViewController gets initialized
private func updatePlayerMediaControls() {
GCKCastContext.sharedInstance().defaultExpandedMediaControlsViewController.setButtonType(.skipPrevious, at: 1)
GCKCastContext.sharedInstance().defaultExpandedMediaControlsViewController.setButtonType(.skipNext, at: 2)
}
I use a function that follows this logic to cast a video or add a video to the queue. Immediately after adding a video to the cast I will add the next video to the queue by setting the appending bool to true.
func loadSelectedItem(_ media: VideoMediaInformation, byAppending appending: Bool) {
if let remoteMediaClient = sessionManager.currentCastSession?.remoteMediaClient {
let mediaQueueItemBuilder = GCKMediaQueueItemBuilder()
mediaQueueItemBuilder.mediaInformation = media.mediaInfo
mediaQueueItemBuilder.autoplay = true
mediaQueueItemBuilder.preloadTime = 1.0
let queueOptions = GCKMediaQueueLoadOptions()
queueOptions.playPosition = media.currentTime ?? 0.0
if appending {
let request = remoteMediaClient.queueInsert(mediaQueueItemBuilder.build(), beforeItemWithID: kGCKMediaQueueInvalidItemID)
request.delegate = self
} else {
let request = remoteMediaClient.queueLoad([mediaQueueItemBuilder.build()], with: queueOptions))
request.delegate = self
GCKCastContext.sharedInstance().presentDefaultExpandedMediaControls()
}
}
}
I would expect that if there are items in the queue that the user would be able to use the skipNext and skipPrevious to skip forward or backward in the queue as episodes are available. The actual results are that the buttons are always disabled.
I worked extensively with this issue. I was experiencing the exact same thing, as well as the issue referenced in the question. I attempted a full update of the library to 4.6.1. Since this package is a static library I was unable to inspect the cause. BUT Good News! I found a suitable work around. This solution does not take into account whether or not there is something available in the queue to move forward or backwards but it met my needs. In addition with the setup above I was able to use custom buttons that trigger the skip backward or forward.
#IBAction func playPressed(_ sender: UIButton) {
// Create and add custom previous button
let prevButton = UIButton()
prevButton.setImage(Icon.mediaPlayerPrevious, for: .normal)
prevButton.addTarget(self, action: #selector(castButtonPrevAction), for: .touchUpInside)
GCKCastContext.sharedInstance()
.defaultExpandedMediaControlsViewController
.setButtonType(.custom, at: 0)
GCKCastContext.sharedInstance()
.defaultExpandedMediaControlsViewController
.setCustomButton(prevButton, at: 0)
// Create and add custom next button
let nextButton = UIButton()
nextButton.setImage(Icon.mediaPlayerNext, for: .normal)
nextButton.addTarget(self, action: #selector(castButtonNextAction), for: .touchUpInside)
GCKCastContext.sharedInstance()
.defaultExpandedMediaControlsViewController
.setButtonType(.custom, at: 3)
GCKCastContext.sharedInstance()
.defaultExpandedMediaControlsViewController
.setCustomButton(nextButton, at: 3)
// Presents the screen
GCKCastContext.sharedInstance().presentDefaultExpandedMediaControls()
}
// Casting custom button actions
#objc func castButtonNextAction(sender: UIButton!) {
GMCastChannel.shared.remoteMediaClient?.queueNextItem()
}
#objc func castButtonPrevAction(sender: UIButton!) {
GMCastChannel.shared.remoteMediaClient?.queuePreviousItem()
}

Cancel UICollectionView updates when view is being dismissed

I have recently reported a crash in my app, and I've found out what is happening and I need some help/best practices/best approach to this issue.
I have a pushed UICollectionViewController that on viewDidLoad queries the server to fetch some data to fill the UICollectionView.
My problem here is, if I push this UICollectionViewController and then tap the back button fast - the background thread still continues to fetch the server data, but when the data is fetched I update the UICollectionView with the performBatchUpdates() and my app crashes.
Here it happens because the app is attempting to reload data on a view that's not visible anymore.
What's the best practice here?
Is there any way to "abort" collection view updates if I'm moving back to the previous VC?
something like:
if self.isMovingFromParentViewController { /* abort any update here? */ }
Thanks
You can use DispatchWorkItem for achieving this as follows
let backgroundQueue = DispatchQueue.global()
var backgroundTask: DispatchWorkItem!
backgroundTask = DispatchWorkItem { [weak self] in
// Perform background task
if !backgroundTask.isCancelled {
return to main Queue
}
backgroundTask = nil // resolve strong reference cycle
}
backgroundQueue.async(execute: backgroundTask)
// When you want to cancel the task
backgroundQueue.async { [weak backgroundTask] in
backgroundTask?.cancel()
}
This is desirable in many cases where we should abort all the Server request. I prefer to perform all the clean up in the
deinit() {
// Abort all your APIs and asynchronous call
// Release all dependency
}
Along with this, always have a weak reference of your controllers and then perform optional binding in the response of the Asynchronous call.
Almofire.request(reqData: param, method: get.....) {
[weak self] response in
guard let safeSelfRef = self, let safeCollectionView =
safeSelfRef.collectionView else { return }
//Update view here
}

Launch function at the end of another swift

I need to launch the funcion "caricaImmagine" after the function "caricaLavori". In the code is clear that caricaImmagine need that caricaLavori finish to append item to an array.
I've tried this but I don't know how to implement it.
Here's the "newMethod" (called in viewWillAppear):
func newMethod(){
print("login: \(login)")
let userFetch = NSFetchRequest<NSFetchRequestResult>(entityName: "UserEntity")
do {
fetchUsers = try moc.fetch(userFetch) as! [User]
} catch {
fatalError("Failed to fetch person: \(error)")
}
do{
let users = try moc.fetch(userFetch)
if(users.count > 0){
print("utente connesso")
tableview.delegate = self
tableview.dataSource = self
let d_email = fetchUsers.first!.email
caricaLavori(datore_email: d_email!)
for i in 0...lavori.count{
caricaImmagine(id_lavoro: lavori[i].id)
}
}else{
DispatchQueue.main.async {
self.performSegue(withIdentifier: "area_utente_segue", sender: self)
}
}
}catch {}
}
If you want the other 2 functions I'll edit this post (both of them loads data from php script, both do an http request)
thank you guys
You say "both of them loads data from php script, both do an http request". That is a key piece of information.
On iOS you want to do network access asynchronously. That means that a function that submits a network requests submits that request and then returns immediately, before the request has even been sent to the remote server.
The solution is to write your methods to take a completion handler. The method invokes the completion handler once the task is complete.
Take a look at my answer in the thread below for code that includes a working example of using completion handlers:
Storing values in completionHandlers - Swift
Note that that post (And the sample app it links to) was written in Swift 2, and will need to be updated for Swift 3.

Refresh TableView except pull to refresh

I use Parse to save data and every time I add new data to server,TableView doesn't refresh and get new data
Is any way to refresh TableView except pull refresh ?
First of all Add Observer method which will notify when you.
NotificationCenter.default.post(name: Notification.Name("ReloadTableData"), object: nil)
Register that observer method in your view controller on which tableview you used.
NotificationCenter.default.addObserver(self, selector: #selector(self.methodOfReceivedNotification(notification:)), name: Notification.Name("ReloadTableData"), object: nil)
and create one function and reload your tableview
This might by your possible solutions for reloading without pull to refresh.
You can use this:
self.tableView.reloadData()
Whenever the user get back to the app or opens the app.
You can add in in the method
applicationWillEnterForeground
If the user is still in the app while the updates are being performed, then use push notification. In the method
appDidReceiveRemoteNotification
reload the data. You have to register for remote notification and all the process before you receive any push notification, but that is another issue.
first of all Adding refresh control to the tableview. So add the below code in ViewDidLoad.
let refresh = UIRefreshControl()
refresh.tintColor = UIColor.redColor()
refresh.attributedTitle = NSAttributedString(string:"Loading..!!", attributes: [NSForegroundColorAttributeName: UIColor.redColor()])
refresh.addTarget(self, action: "handleRefresh:", forControlEvents: UIControlEvents.ValueChanged)
self.tableView.addSubview(refresh)
self.tableView.sendSubviewToBack(refresh)
Now Calling the method when you Pull-To-refresh Tableview.
func handleRefresh(refreshControl : UIRefreshControl){
self.tableView.reloadData()
refreshControl.endRefreshing()
}
Refresh Control :
Implement a push notification or use LiveQuery on parse-server.
LiveQuery allows you to subscribe to a Parse.Query you are interested in. Once subscribed, the server will notify clients whenever a Parse.Object that matches the Parse.Query is created or updated, in real-time.
LiveQuery is already support IOS.
for more detail see the hyperLink.
https://github.com/ParsePlatform/parse-server
https://github.com/ParsePlatform/parse-server/wiki/Parse-LiveQuery
https://github.com/ParsePlatform/ParseLiveQuery-iOS-OSX

Why does Example app using Photos framework use stopCachingImagesForAllAssets after each change?

So I'm looking at the Photos API and Apple's sample code here
https://developer.apple.com/library/ios/samplecode/UsingPhotosFramework/Introduction/Intro.html
and its conversion to swift here
https://github.com/ooper-shlab/SamplePhotosApp-Swift
I have integrated the code into my project so that a collectionView is successfully updating itself form the library as I take photos. There is one quirk: Sometimes cells are blank, and it seems to be connected to stopCachingImagesForAllAssets which Apple calls each time the library is updated at the end of photoLibraryDidChange delegate method.
I can remove the line and it fixes the problem, but surely there is a reason Apple put it there in the first place? I am concerned with memory usage.
// MARK: - PHPhotoLibraryChangeObserver
func photoLibraryDidChange(changeInstance: PHChange) {
// Check if there are changes to the assets we are showing.
guard let
assetsFetchResults = self.assetsFetchResults,
collectionChanges = changeInstance.changeDetailsForFetchResult(assetsFetchResults)
else {return}
/*
Change notifications may be made on a background queue. Re-dispatch to the
main queue before acting on the change as we'll be updating the UI.
*/
dispatch_async(dispatch_get_main_queue()) {
// Get the new fetch result.
self.assetsFetchResults = collectionChanges.fetchResultAfterChanges
let collectionView = self.pictureCollectionView!
if !collectionChanges.hasIncrementalChanges || collectionChanges.hasMoves {
// Reload the collection view if the incremental diffs are not available
collectionView.reloadData()
} else {
/*
Tell the collection view to animate insertions and deletions if we
have incremental diffs.
*/
collectionView.performBatchUpdates({
if let removedIndexes = collectionChanges.removedIndexes
where removedIndexes.count > 0 {
collectionView.deleteItemsAtIndexPaths(removedIndexes.aapl_indexPathsFromIndexesWithSection(0))
}
if let insertedIndexes = collectionChanges.insertedIndexes
where insertedIndexes.count > 0 {
collectionView.insertItemsAtIndexPaths(insertedIndexes.aapl_indexPathsFromIndexesWithSection(0))
}
if let changedIndexes = collectionChanges.changedIndexes
where changedIndexes.count > 0 {
collectionView.reloadItemsAtIndexPaths(changedIndexes.aapl_indexPathsFromIndexesWithSection(0))
}
}, completion: nil)
}
self.resetCachedAssets() //perhaps prevents memory warning but causes the empty cells
}
}
//MARK: - Asset Caching
private func resetCachedAssets() {
self.imageManager?.stopCachingImagesForAllAssets()
self.previousPreheatRect = CGRectZero
}
I was having the same result.
Here's what fixed the issue for me:
Since performBatchUpdates is asynchronous, the resetCachedAssets gets executed possibly while the delete/insert/reload is happening, or even between those.
That didn't sound nice to me. So I moved the line:
self.resetCachedAssets()
to the first line of the dispatch_async block.
I hope this helps you too.

Resources