I'm using a UIRefreshControl to enable the pull-to-refresh gesture on a table view.
I have used the storyboard te setup the RefershControl and in my TableViewController is use the following code to bound the method to the RefeshControl:
self.refreshControl?.addTarget(self, action: Selector("getData"), forControlEvents: UIControlEvents.ValueChanged)
At the end of the getData() method I call the reloadData() method on the tableview and the stopRefreshing() method on the refreshcontrol.
This is working fine. I can pull to refresh and the table gets updated.
Next thing I want is to start the RefreshControl when the TableViewController gets loaded. To show the user that the app is getting the data.
I tried to manually start the folowing code:
self.refreshCorntrol?.beginRefreshing()
getData()
It reloads the data but the animation is not working like it should.
The table stays empty. Then when all the data is fetched the table is pulled down (like when I manually pull to refresh) en immediately pushed back up.
Anyone know if it is possible to change/fix this?
Two things could be going on.
1. UI Updates must be dispatched on the main thread.
You're updating a tableView using it's .reloadData() method, which is considered an update to the UI. This being said, you must call .reloadData() on the main thread. While this should be done automatically for you already, I have run into some cases where I need to manually move it to the main thread. This can be done using
DispatchQueue.main.async {
// Update UI ie. self.tableView.reloadData()
}
inside your function.
2. Programmatic pull refreshes don't always run for .valueChanged
It is important to call .valueChanged (or, as you have it in your code, UIControlEvents.ValueChanged) when you call the pull refresh function, so that it is aware of what you are refreshing for. When the user pull refreshes, your program already does it, in the forControlEvents segment of your refreshController declaration. However, when you call it manually from your code, it does not do this by default. Instead, you have to pass the action using sendActions. This can be done by using
self.refreshControl?.beginRefreshing()
self.refreshControl?.sendActions(for: UIControlEvents.ValueChanged)
getData()
to call the pull refresh, rather than just calling .beginRefreshing
Afterword
I ran into a similar issue a few days ago, and posted the question on Stack Overflow here (I'm providing attribution in case you can find your answer there, and because it is where I took my first solution for you from).
Related
I ran into a problem with my UI, which is not updating immediately.
I am calling someCustomView.isHidden = false first. After that I create a new instance of a new View Controller. Inside the new VCs viewDidLoad(), I am loading a "new Machine Learning Model", which takes some time.
private func someFuncThatGetsCalled() {
print("1")
self.viewLoading.isHidden = false
print("2")
performSegue(withIdentifier: "goToModelVCSegue", sender: nil)
}
As soon as I press the button that calls this function, "1" and "2" is printed in the console. However the view is not getting visible before the viewDidLoad() of my new VC is finished.
Is there any possibility to force update a UIView immediately? setNeedsDisplay() did not work for me.
Thanks for your help!
Use layoutIfNeeded() Apple Docs
layoutIfNeeded()
Lays out the subviews immediately, if layout updates are pending.
Use this method to force the view to update its layout immediately. When using Auto Layout, the layout engine updates the position of views as needed to satisfy changes in constraints. Using the view that receives the message as the root view, this method lays out the view subtree starting at the root. If no layout updates are pending, this method exits without modifying the layout or calling any layout-related callbacks.
So As a rule of thumb,
layoutifneeded : Immediate (current update cycle) , synchronous call
setNeedsLayout :relaxed ( wait till Next Update cycle) , asynchronous call
So, layoutIfNeeded says update immediately please, whereas setNeedsLayout says please update but you can wait until the next update cycle.
how to use
yourView.layoutIfNeeded()
You can also refer to the diagram to better remember the order of these passes
Source Apple docs on layoutIfNeeded
Image credit Medium artcle
A couple problems...
If you have a view controller that "takes some time" to load, you should not try to do it in that manner.
The app will be non-responsive and appear "frozen."
A much better approach would be:
on someFuncThatGetsCalled()
hide viewLoading and replace it with an activity indicator (spinner, or something else that let's the user know the app is not stuck)
instantiate your ModelVC
when ModelVC has finished its setup, have it inform the current VC (via delegate)
current VC then shows / navigates to the already instantiated and prepared ModelVC
Or, probably a better option... Move your time-consuming setup in ModelVC to a point after the view has appeared. You can show an activity indicator in viewDidLoad(). That is really the most common UX - you see it all the time when the new VC has to retrieve remote data to display - and it would fit wit what users have come to expect.
Here is the scenario:
OrderVC have a table view on which if you right swipe it shows some
options. In which one of the option is Checkout.
When user taps on Checkout it opens up CheckoutVC which has a parent class OrderVC.
Here user can add some text and can attach multiple images and can also save this data as draft which is achieved using core data. But when user submit the bill I'm using AFNetworking to call web api and upload images using AFMultipartFormData. All of this process is taking place on a background thread i.e.dispatch_async
I can't update UI in dispatch_get_main_queue because methods are calling other method from within see this question it'll clear this point. So it calls the update UI right after first method is finished.
Question
As long as background thread is working it should show activity indicator on the cell. When it's finished and the response is success in CheckoutVC it should reload the tableView of OrderVC.
Solution I triedI tried to run a for loop in allOrderID which are the ID's I get via web api hit of active orders. Then I made a call to MR_findFirstByAttribute to find if any of the fetched OrderID exist in drafts. There is an attribute isSending in DraftOrderInfo entity which is a BOOL and I truns it to true when checkout enters background thread. So if isSending is true I show the activity indicator in place of a UIView I created.
for (NSString *orderID in allOrderId) {
DraftOrderInfo *dpi = [DraftOrderInfo MR_findFirstByAttribute:#"orderID" withValue:orderID];
if (dpi.isSending) {
orderCell.rightUtilityButtons = nil;
activityView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
activityView.center = CGPointMake(orderCell.orderStatusIndicatorBadgeView.frame.origin.x-2, orderCell.orderStatusIndicatorBadgeView.frame.origin.y-8);
[activityView startAnimating];
[orderCell.orderStatusIndicatorBadgeView addSubview:activityView];
}
}
The output I get is that when OrderVC is loaded it started showing activity indicator on all the cells.
First, your following point is not valid. Read the comments on the question:
can't update UI in dispatch_get_main_queue because methods are calling
other method from within see this question it'll clear this point. So
it calls the update UI right after first method is finished.
Back to the problem. There is multiple good practices to use. One of them:
Add the UIActivityIndicator to the UITableViewCell at design time(nib or storyboard) as done with the UILabel(s) and other controls.
On submitting the checkout. Change the isSending status to YES and inform the parent UIViewController to reload the table data by calling reloadData method on the UITableView.
In cellForRowAtIndexPath or willDisplay:forRowAt method set the state of the activity indicator as animating or stopped based on the isSending value. This way even if reloading the table or scrolling up and down, the activity indicator will have its state right.
Whe the submitting finishes. Change the isSending state to YES and inform your parent ViewController to reload the table by calling reloadData method on the UITableView. And since the checkin finishes in a background thread you should inform your parent ViewController using dispatch_get_main_queue. read the comments on the question you added to your question. This point you mention regarding the dispatch_get_main_queue is wrong.
I have a simple app in Swift with just a few views:
A UIWebView
some TableViews
and another view with some data I download from my server
It all works well until when using the app I press the home button, leave there for a while then the iPad goes on sleep mode. A few days later I tap on the app icon and it won't start:
first tap on the icon will select the icon (goes a little darker) and deselect it a few seconds later
second tap will launch the LaunchScreen and crash a few seconds later
double tap the home button and quit the app will sometimes work
I'm just wondering if there is something I need to set on my code to handle idle/long periods of inactivity in something like viewWillDisappear or other methods?
If so I already have this in all my controllers:
override func viewWillDisappear(animated: Bool) {
timer.invalidate()
webView.removeFromSuperview()
}
Maybe I need to call super. in there too? or something else I'm missing?
You should definitely call super in your viewWillDisappear(animated:) method. See UIViewController Class Reference documentation. Also you might want to confirm why you are removing your webView from the view controller's hierarchy.
Discussion
This method is called in response to a view being removed
from a view hierarchy. This method is called before the view is
actually removed and before any animations are configured.
Subclasses can override this method and use it to commit editing
changes, resign the first responder status of the view, or perform
other relevant tasks. For example, you might use this method to revert
changes to the orientation or style of the status bar that were made
in the viewDidDisappear: method when the view was first presented. If
you override this method, you must call super at some point in your
implementation.
You probably have some null pointer exception and crash. Maybe you are calling some variable that is not set (and checked if not null).
Try disabling app funcionality (like downloading, storing and using data from server) and see where you app starts working normal again and then procede from there.
Sorry for vague answer but withouth code and maybe some log it is really hard to give specific answer.
And NO, you dont have to do anything special to handle idle/long periods of inactivity.
in a ViewController, that hosts a TableView, I load the data for the table view using AFNetworking in viewDidLoad.
This can take a couple of seconds. If during the network call, the users swipes back to the previous VC, the app crashes. I think it is because the network call is still running, comes back and the original callback for the network call is somehow lost?
I started using stuff like "userInteractionEnabled = false" and setting it back to true when the data finished loading or setting a global variable in viewWillDisappear and checking this in the callback. This works but seems quite wrong.
What is the proper method/func to load data for a table view?
How is a situation like this properly handled, e.g. should I cancel all AFNetworking request in viewWillDisappear?
Thanks a lot for a hint!
This is how my application is looking now:
After I perform a database update in my detail controller in view number 7 in the image above as soon as the save button is clicked the details are saved the the database. I'm taken back to tableView number 5 and expect the associated row to show latest updates by calling a special method from the parse.com framework that reloads objects and refreshes the table view e.g. [self loadObjects].
I use an unwind segue. In view 7 I make a connection between the save button and the exit symbol of it's controller window in interface builder and then in tableView number 5 I have my segue method that corresponds to this connect.
Unwind segue method:
-(IBAction)saveDetailsButtonTapped:(UIStoryboardSegue *)segue {
// alert goes here
[self performSelector:#selector(didTapRefreshButton:) withObject:self afterDelay:1.0];
}
This method clears the table and loads the first page of objects:
- (IBAction)didTapRefreshButton:(id)sender {
[self loadObjects];
}
When save is clicked on view number 7 the details are saved to the db and user is bought back to table view number 5 then the method above runs after 1.0 delay. I thought this was ok but didn't feel too right. I tried it on my phone and sometimes the delay wasn't long enough, meaning a failed refresh.
I then decided to try using a UIAlertView delegate method to detect when the ok button of the alertview was pressed and it worked ok most times but then the times I pressed OK to dismiss the alert really quickly upon arriving back on the view and the data wasn't reloaded.
Is there a better solid reliable way to refresh my data?
I need some way of knowing that the database update was successful and only then run the [self loadObjects] method and maybe do that automatically.
I have two methods that detect when objects will load (e.g. like when a button has been tapped) and when they have loaded. I have put some spinner code in there to show a spinner while loading is happening and take it away once it's done.
Isn't there some sort of way to queue methods, like some how in one method make it so one thing doesn't happen until another thing has happened?
If so, I'd really appreciate some insight and examples as I could just mark the app as complete but even though I'm not being paid and it's charity work I still have the urge to do my best.
Thanks for your time.
Kind regards
I have put some spinner code in there to show a spinner while loading is happening and take it away once it's done.
You should do something like that here.
I need some way of knowing that the database update was successful and only then run the [self loadObjects] method and maybe do that automatically.
Because you're saving to parse, it should be the parse SDK that tells you when the save is complete. If you're saving in the background (which you should be) then use the save method when provides you with a callback block that is called when the save has completed. This block being called is your trigger to remove the spinner and segue.
Side note :-
Yes, there are several different kinds of queues, most better than using performSelector:..., but there are also other ways of working with asynchronous activities and you should look at the asynchronous activity for guidance. i.e. can I get a callback when this is done, rather than how long should I wait and hope that it is done.