Delay when changing UINavigationController title with swift - iphone

Ok, I have tried different ways to update the title in the UINavigationBar but there is always a delay.
I have a Navigation Controller that is a root view controller of a table view. I have tried different ways to change the title and try to get the UINavigationBar to redraw, but there is a delay of ~10 seconds.
//I have tried different ways to change the title:
self.title = str
//and
#IBOutlet var tableTitle: UINavigationItem!
self.tableTitle.title = str
//as well as different ways to convince it to redraw
self.view.window?.rootViewController?.setNeedsStatusBarAppearanceUpdate()
//and
self.tableTitle.awakeFromNib()
I forgot that this was being done from a dataTaskWithURL, so it needed to be called from the main thread.

Try using the dispatch_async, this function submits a block for asynchronous execution on a dispatch queue and returns immediately, take a look in the following code:
dispatch_async(dispatch_get_main_queue(), {
// Change your title here
})
I hope this help you.

Related

Removing a view controller from UIPageViewController

It's odd that there's no straightforward way to do this. Consider the following scenario:
You have a page view controller with 1 page.
Add another page (total 2) and scroll to it.
What I want is, when the user scrolls back to the first page, the 2nd page is now removed and deallocated, and the user can no longer swipe back to that page.
I've tried removing the view controller as a child view controller after the transition is completed, but it still lets me scroll back to the empty page (it doesn't "resize" the page view)
Is what I want to do possible?
While the answers here are all informative, there is an alternate way of handling the problem, given here:
UIPageViewController navigates to wrong page with Scroll transition style
When I first searched for an answer to this problem, the way I was wording my search wound me up at this question, and not the one I've just linked to, so I felt obligated to post an answer linking to this other question, now that I've found it, and also elaborating a little bit.
The problem is described pretty well by matt here:
This is actually a bug in UIPageViewController. It occurs only with the scroll style (UIPageViewControllerTransitionStyleScroll) and only
after calling setViewControllers:direction:animated:completion: with
animated:YES. Thus there are two workarounds:
Don't use UIPageViewControllerTransitionStyleScroll.
Or, if you call setViewControllers:direction:animated:completion:, use
only animated:NO.
To see the bug clearly, call
setViewControllers:direction:animated:completion: and then, in the
interface (as user), navigate left (back) to the preceding page
manually. You will navigate back to the wrong page: not the preceding
page at all, but the page you were on when
setViewControllers:direction:animated:completion: was called.
The reason for the bug appears to be that, when using the scroll
style, UIPageViewController does some sort of internal caching. Thus,
after the call to setViewControllers:direction:animated:completion:,
it fails to clear its internal cache. It thinks it knows what the
preceding page is. Thus, when the user navigates leftward to the
preceding page, UIPageViewController fails to call the dataSource
method pageViewController:viewControllerBeforeViewController:, or
calls it with the wrong current view controller.
This is a good description, not quite the problem noted in this question but very close. Note the line about if you do setViewControllers with animated:NO you will force the UIPageViewController to re-query its data source next time the user pans with a gesture, as it no longer "knows where it is" or what view controllers are next to its current view controller.
However, this didn't work for me because there were times when I need to programmatically move the PageView around with an animation.
So, my first thought was to call setViewControllers with an animation, and then in the completion block call the method again with whatever view controller was now showing, but with no animation. So the user can pan, fine, but then we call the method again to get the page view to reset.
Unfortunately when I tried that I started getting strange "assertion errors" from the page view controller. They look something like this:
*** Assertion failure in -[UIPageViewController queuingScrollView: ...
Not knowing exactly why this was happening, I backtracked and eventually started using Jai's answer as a solution, creating an entirely new UIPageViewController, pushing it onto a UINavigationController, then popping out the old one. Gross, but it works--mostly. I have been finding I'm still getting occasional Assertion Failures from the UIPageViewController, like this one:
*** Assertion failure in -[UIPageViewController queuingScrollView:didEndManualScroll:toRevealView:direction:animated:didFinish:didComplete:],
/SourceCache/UIKit_Sim/UIKit-2380.17/UIPageViewController.m:1820 $1 =
154507824 No view controller managing visible view >
And the app crashes. Why? Well, searching, I found this other question that I mentioned up top, and particularly the accepted answer which advocates my original idea, of simply calling setViewControllers: animated:YES and then as soon as it completes calling setViewControllers: animated:NO with the same view controllers to reset the UIPageViewController, but it had the missing element: calling that code back on the main queue! Here's the code:
__weak YourSelfClass *blocksafeSelf = self;
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:^(BOOL finished){
if(finished)
{
dispatch_async(dispatch_get_main_queue(), ^{
[blocksafeSelf.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:NULL];// bug fix for uipageview controller
});
}
}];
Wow! The only reason this actually made sense to me is because I have watched the the WWDC 2012 Session 211, Building Concurrent User Interfaces on iOS (available here with a dev account). I recall now that attempting to modify data source objects that UIKit objects (like UIPageViewController) depend on, and doing it on a secondary queue, can cause some nasty crashes.
What I have never seen particularly documented, but must now assume to be the case and read up on, is that the completion block for an animation is performed on a secondary queue, not the main one. So the reason why UIPageViewController was squawking and giving assertion failures, both when I originally attempted to call setViewControllers animated:NO in the completion block of setViewControllers animated:YES and also now that I am simply using a UINavigationController to push on a new UIPageViewController (but doing it, again, in the completion block of setViewControllers animated:YES) is because it's all happening on that secondary queue.
That's why that piece of code up there works perfectly, because you come from the animation completion block and send it back over to the main queue so you don't cross the streams with UIKit. Brilliant.
Anyway, wanted to share this journey, in case anyone runs across this problem.
EDIT: Swift version here, if anyone's interested.
Concluding Matt Mc's great answer, the following method could be added to a subclass of UIPageViewController, that way allowing the usage of setViewControllers:direction:animated:completion: as it was intended to be used if the bug would not be present.
- (void) setViewControllers:(NSArray*)viewControllers direction:(UIPageViewControllerNavigationDirection)direction animated:(BOOL)animated completion:(void (^)(BOOL))completion {
if (!animated) {
[super setViewControllers:viewControllers direction:direction animated:NO completion:completion];
return;
}
[super setViewControllers:viewControllers direction:direction animated:YES completion:^(BOOL finished){
if (finished) {
dispatch_async(dispatch_get_main_queue(), ^{
[super setViewControllers:viewControllers direction:direction animated:NO completion:completion];
});
} else {
if (completion != NULL) {
completion(finished);
}
}
}];
}
Now, simply call setViewControllers:direction:animated:completion: on the class/subclasses implementing this method, and it should work as expected.
maq is right. If you are using the scrolling transition, removing a child view controller from the UIPageViewController does not prevent the deleted "page" from returning on-screen if the user navigates to it. If you're interested, here's how I removed the child view controller from the UIPageViewController.
// deleteVC is a child view controller of the UIPageViewController
[deleteVC willMoveToParentViewController:nil];
[deleteVC.view removeFromSuperview];
[deleteVC removeFromParentViewController];
View controller deleteVC is removed from the childViewControllers property of the UIPageViewController, but still appears on-screen if the user navigates to it.
Until someone smarter than me finds an elegant solution, here's a work around (it's a hack--so you have to ask yourself if you really need to remove pages from a UIPageViewController).
These instructions assume that only one page is displayed at a time.
After the user taps a button indicating that she would like to delete the page, navigate to the next or previous page using the setViewControllers:direction:animated:completion: method. Of course, you then need to delete the page's content from your data model.
Next (and here's the hack), create and configure a brand new UIPageViewController and load it in the foreground (i.e., in front of the other UIPageViewController). Make sure that the new UIPageViewController starts off displaying the exact same page that was previously displayed. Your new UIPageViewController will fetch fresh view controllers from the data source.
Finally, unload and destroy the UIPageViewController that's in the background.
Anyway, maq asked a really good question. Unfortunately, I don't have enough reputation points to up vote the question. Ah, dare to dream... someday I will have 15 reputation points.
I'll put this answer here just for my own future reference and if it helps anyone - what I ended up doing was:
Delete the page and advance to the next page
In the completion block of setViewControllers, I created/init'ed a new UIPageViewController with the modified data (item removed), and pushed it without animating, so nothing changes on-screen (my UIPageViewController is contained within a UINavigationController)
After pushing the new UIPageViewController, get a copy of the viewControllers array of the UINavigationController, remove the second-to-last view controller (which is the old UIPageViewController)
There is no step 4 - done!
I am just learning this myself, so take with a grain of salt, but from what I understand, you need to change the datasource of the pageviewcontroller, not remove the viewcontroller. How many pages are shown in a pageviewcontroller is determined by its datasource, not the viewcontrollers.
For improve this. You should detect whether pageView is scrolling or not before setViewControllers.
var isScrolling = false
func viewDidLoad() {
...
for v in view.subviews{
if v.isKindOfClass(UIScrollView) {
(v as! UIScrollView).delegate = self
}
}
}
func scrollViewWillBeginDragging(scrollView: UIScrollView){
isScrolling = true
}
func scrollViewDidEndDecelerating(scrollView: UIScrollView){
isScrolling = false
}
func jumpToVC{
if isScrolling { //you should not jump out when scrolling
return
}
setViewControllers([vc], direction:direction, animated:true, completion:{[unowned self] (succ) -> Void in
if succ {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.setViewControllers([vc], direction:direction, animated:false, completion:nil)
})
}
})
}
This problem exists when you are trying to change viewControllers during swipe gesture animation between viewControllers. To solve this problem, i made a simple flag to discover when page view controller is during transition.
- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers {
self.pageViewControllerTransitionInProgress = YES;
}
- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed {
self.pageViewControllerTransitionInProgress = NO;
}
And when i am trying to chenge view controller i am checking if there is transition in progress.
- (void)setCurrentPage:(NSString *)newCurrentPageId animated:(BOOL)animated {
if (self.pageViewControllerTransitionInProgress) {
return;
}
[self.pageContentViewController setViewControllers:#[pageDetails] direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:^(BOOL finished) {
}
}
Actually I think I solved the issue. This is what was happening in my app:
UIViewController subclass instance was removed from UIPageViewController by removing it from model and setting viewControllers property of UIPageViewController to different ViewController. This is enough to do the job. No need to do any controller containment code on that controller
The ViewController was gone, but I could still scroll to it by swiping right in my case.
My issue was this. I was adding a custom gesture recognizer to ViewController displayed by UIPageViewController. This recognizer was hold as a strong property on the controller that also owned UIPageViewController.
FIX: before loosing access to the ViewController being dismissed I made sure I properly cleaned all the memory it uses(dealloc) and removed the gesture recognizer
Your mileage may vary, but I see no point for this solution to be wrong, when something's not working I first suspect my code :)
I had a similar situation where I wanted the user to be able to "tap and delete" any page from the UIPageViewController. After playing a bit with it, I found a simpler solution than the ones described above:
Capture the page index of the "dying page".
Check to see if the "dying page" is the last page.
This is important because if it is the last page, we need to scroll left (if we have pages "A B C" and delete C, we will scroll to B).
If it is not the last page, we will scroll right (if we have pages "A B C" and delete B, we will scroll to C).
Make a temporary jump to a "safe" place (ideally the final one). Use animated: NO to have this happen instantaneously.
UIViewController *jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1];
[self setViewControllers:#[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:NO completion:nil];
Delete the selected page from the model/datasource.
Now, if it is the last page:
Adjust your model to select the page on the left.
Get the viewcontroller on the left, note that THIS IS THE SAME ONE than the one you retrieved in step 3.
It is important to do this AFTER you have deleted the page from the datasource because it will refresh it.
Jump to it, this time with animated: YES.
jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1];
[self setViewControllers:#[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:YES completion:nil];
In the case where it was NOT the last page:
Adjust your model to select the page on the right.
Get the viewcontroller on the right, note that this is not the one you retrieved in step 3. In my case, you will see that it is the one at dyingPageIndex, because the dying page has already been removed from the model.
Again, it is important to do this AFTER you have deleted the page from the datasource because it will refresh it.
Jump to it, this time with animated: YES.
jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex;
[self setViewControllers:#[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:nil];
That's it! This works well in XCode 6.1.1.
Full code below. This code is in my UIPageViewController and is called through a delegate from the page to be deleted. In my case, deleting the first page was not allowed as it contains different things from the rest of the pages. Of course, you need to substitute:
YourUIViewController: with the class of your individual pages.
YourTotalNumberOfPagesFromModel: with the total number of pages in your model
[YourModel deletePage:] with the code to delete the dying page from your model
- (void)deleteAViewController:(id)sender {
YourUIViewController *dyingGroup = (YourUIViewController *)sender;
NSUInteger dyingPageIndex = dyingGroup.pageIndex;
// Check to see if we are in the last page as it's a special case.
BOOL isTheLastPage = (dyingPageIndex >= YourTotalNumberOfPagesFromModel.count);
// Make a temporary jump back - make sure to use animated:NO to have it jump instantly
UIViewController *jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1];
[self setViewControllers:#[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:NO completion:nil];
// Now delete the selected group from the model, setting the target
[YourModel deletePage:dyingPageIndex];
if (isTheLastPage) {
// Now jump to the definitive controller. In this case, it's the same one, we're just reloading it to refresh the data source.
// This time we're using animated:YES
jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1];
[self setViewControllers:#[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:YES completion:nil];
} else {
// Now jump to the definitive controller. This reloads the data source. This time we're using animated:YES
jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex];
[self setViewControllers:#[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:nil];
}
}
NSMutableArray *mArray = [[NSMutableArray alloc] initWithArray:self.userArray];
[mArray removeObject:userToBlock];
self.userArray = mArray;
UIViewController *startingViewController = [self viewControllerAtIndex:atIndex-1];
NSArray *viewControllers = #[startingViewController];
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionReverse animated:NO completion:nil];
I didn't quite have time to read through the above comments but this worked for me. Basically I remove the data (in my case a user) and then move to the page before it. Works like a charm. Hope this helps those looking for a quick fix.
It did it the following in Swift 3.0
fileprivate var isAnimated:Bool = false
override func setViewControllers(_ viewControllers: [UIViewController]?, direction: UIPageViewControllerNavigationDirection, animated: Bool, completion: ((Bool) -> Void)? = nil) {
if self.isAnimated {
delay(0.5, closure: {
self.setViewControllers(viewControllers, direction: direction, animated: animated, completion: completion)
})
}else {
super.setViewControllers(viewControllers, direction: direction, animated: animated, completion: completion)
}
}
extension SliderViewController:UIPageViewControllerDelegate {
func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
self.isAnimated = true
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
self.isAnimated = finished
}
}
And here the delay function
func delay(_ delay:Double, closure:#escaping ()->()) {
DispatchQueue.main.asyncAfter(
deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: closure)
}
we are getting crash on tab change .So one solution i got is didChangeSelect of tabs disable the userintraction.

UIPickerView on Separate Page

I am working on an iPhone app. Initially, I had my pickerview in the same screen so this was just a one page app. After skinning it i realized that i want the pickerview on it's own separate page. So i did that. However, my pickerview originally would update uilabels and other objects on that same page. How can I have my pickerview access those objects from it's new view?
- (IBAction)ShowPickerAction:(id)sender {
if (self.theView == nil) {
theView = [containerView initWithNibName:#"containerView" bundle:nil];
theView.parentView = self;
}
self.theView.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
[self presentModalViewController:self.theView animated:YES];
}
That is the method I am using to call my new view. But line 3 of the above code gives me the error "No known class name for selector initWithNibName:bundle". I think that error is related to something that i did wrong in my header file. My new class is called containerView. So i did this in my header:
#interface ViewController : UIViewController {
containerView *theView;
But that gives me the error "Unknown type name containerView" even though i do have a class named containerView!!
Look into uiappdelegate protocol or try passing values to through a static function to the previous page.
Use a delegate to pass information back and forth to the view object that instantiatrd the picker view. You want to keep your code coupling as loose as possible, especially if you might like to drop it into your next project. Using a delegate and/or blocks are some of the best ways.

Strange behaviour in viewWillAppear

I have a TabBar Controller with some tab bar item in it.
The first time that a user tap on a tab bar item, I want that a alertview is opened, so that the user can read some little instruction tips.
I have a global variable (say CONFIG), that hold some boolean valeus (CONFIG.tip1AlreadySeen, CONFIG.tip1AllreadySeen, etc.). All these boolean values are initializated to NO.
When the user tap a tab bar item, the viewWillAppear method in its viewcontroller is executed. In this method I put a code like this one:
-(void) viewVillAppear: (BOOL) animated {
extern CONFIG; // <- it's not the actual code but it indicates that a global variable must be used
[super viewWillAppear: animated];
if(CONFIG.tip1AlreadySeen == NO) {
CONFIG.tip1AlreadySeen = YES;
// code for showing the alertview
}
}
The strange thing is that this piece of code works perfectly in one viewcontroller but doesn't work in one another.
With some debug, I fidd out that in the another viewcontroller the code is executed but the assigment CONFIG.tipAlreadySeen = YES doesn't modify the actual value of CONFIG.tipAlreadySeen. This value is still NO. Unbelievable!!!
A little workaround was using the viewDidAppear method for changing the value:
-(void) viewVillAppear: (BOOL) animated {
extern CONFIG; // <- it's not the actual code but it indicates that a global variable must be used
[super viewWillAppear: animated];
if(CONFIG.tip1AlreadySeen == NO) {
// code for showing the alertview
}
}
-(void) viewDidAppear: (BOOL) animated {
extern CONFIG;
CONFIG.tip1AlreadySeen = YES;
}
...But I really did not understand what happened!!! Someone of you could explain this behaviour?
Thanks in advance!
Marco
Why must this be global and not contained in the view controller itself? Just a simple BOOL #property on your view controller that is toggled. And, to maintain this persistent across multiple runs of your application, save out the result to NSUserDefaults, which you in turn check each time you init your view controller.

iOS - Navigation Title Not Updating

What I'm doing with this bit of code is grabbing an image sequence, grabbing it's name. Then setting the view's title as the sequences name playing the sequence then setting the title back to the old title.
Problem is the title doesn't seem to be changing on the navigation bar. The NSLogs are outputting the correct values though.
I remember having this issue before and solving it with some "refresh" method.
Here is the pertinent code.
-(void)playSequence
{
if (([animatorViewController isAnimating] == FALSE) && (btnDisable == FALSE))
{
Sequence *tempSequence;
tempSequence = [fullStepList objectAtIndex:lastPlayedSequence];
self.title = [NSString stringWithFormat:#"Assembly - %#", tempSequence.subName];
NSLog(self.title);
[self startAnimator:tempSequence.imageNameScheme forNumFrames:tempSequence.numberOfFrames playInReverse:FALSE];
tempSequence = nil;
tempSequence = [fullStepList objectAtIndex:queuedSequence];
self.title = [NSString stringWithFormat:#"Assembly - %#", tempSequence.subName];
NSLog(self.title);
tempSequence = nil;
}
}
EDIT: This method of changing the title is working else where in this class. The problem seems to come in when trying to set it twice that is causing the issue.
EDIT2: It's actually running both title changes one after another... Fact that it was setting it back to previous was throwing me off.
A navigation item may contains four things: leftBarButtonItem, rightBarButtonItem, title, and titleView. When you need to change the title you should assign your title to navigationItem's title property, not to your view's title property. So it would look like:
self.navigationItem.title = [NSString stringWithFormat:#"Assembly - %#", tempSequence.subName];
self.title points to your viewControllers title which don't shows up in the navigationBar.
The title won't appear to update until after the main run loop executes. Unless -startAnimator:forNumFrames: invokes the main run loop to execute before it returns, you won't see the change.
If you are hosting the UINavigationController inside another view, you need to do this:
self.yourNavigationController.title = #"title";
instead of
self.title = #"blah"
Otherwise you are just changing the title of the view hosting the view navigator. Just a guess.
I recently found myself in a similar situation. My view's title was comprised of a label with an NSMutableAttributedString. Calling [self reloadInputViews] whenever I updated my title worked for me.
The dreaded P.E.B.K.A.C. error was the cause of this confusion. It's actually running both title changes one after another... Fact that it was setting it back to previous was throwing me off.

how do i add two delegates to a ui element at run time?

im trying to implement some behaviors when a mapview element scrolls... by coding a delegate for the scrollview inside of a mapview.
so, right now, i got a pointer to the scroll view used by the map view in my code.
however, i wish to set the delegate of this scroll view inside the map view, but the issue is that the mapview already sets up a default delegate for this scroll view inside the map view.
can i make my delegate implement all of the messages of the protocol, explicitly sending them to the mapview's default delegate while also implementing my own behaviors?
how else can i go about adding my own delegate behavior, to an already existing default delegate....?
thanks everyone,
michael
You could just get the existing delegate and save a reference for yourself:
origDelegate = [theView delegate];
And then set the object you want as the delegate:
[theView setDelegate:self];
Then when getting a delegate message, call the same method on origDelegate, modify the response if you want to (or if necessary), and then return the modified response:
- (BOOL)shouldViewDoSomething:(id)theView
{
BOOL result = [origDelegate shouldViewDoSomething:theView];
if (decision1)
{
result = !result;
}
return result;
}