Pre-render UITabBarController views? - iphone

I have an app with a UITabBarController that manages some UINavigationControllers, which in turn manage various UIViewControllers.
On 3G phones, the first time I view any particular view via a TabBar button, it's laggy, but thereafter, it's snappy. This isn't noticeable with 3GS phones. My question is how can I force these views to pre-render? I have tried triggering the loadView functions by calling them in a different thread on start-up, but this doesn't do anything I don't think.
For clarity, here is an abbreviated snippet from my code to show what I'm doing. I have five view controllers, but I'm showing just the code for two. The poiVC is just a standard UITableViewController subclass - I don't even have a custom init or loadView function for it.
- (void)applicationDidFinishLaunching:(UIApplication *)application {
self.mapVC = [[MapViewController alloc] init];
NavControlBar * mapNavBar = [[[NavControlBar alloc] initWithViewController:mapVC
withControlBar:[mapVC initBar]]autorelease];
self.poiVC = [[POIViewController alloc] init];
NavControlBar * poiNavBar = [[[NavControlBar alloc] initWithViewController:poiVC
withControlBar:[poiVC initBar]]autorelease];
NSArray *tabViewControllerArray = [NSArray arrayWithObjects:mapNavBar, poiNavBar, nil];
self.tbc.viewControllers = tabViewControllerArray;
self.tbc.delegate = self;
[mapVC release];
[poiVC release];
[window addSubview:tbc.view];
}
Can I get the poiVC to render while the user is looking at the first screen, so the transition will be fast?

[self.mapVC view];
[self.poiVC view];
You can simply ask for the view of the view controller and not do anything with it. This will return the view, and hence load it for you if needed. The disadvantage is of course that you'll increase your startup time. Also note that your view may be unloaded when memory runs low, which causes lagginess again when switching to those tabs, but (at least tries to) keep your app running (generally considered a good thing).

I've been struggling with the same issue. All of the answers I've seen talk about trying to preload the view by making a call viewController.view, but that's not the main bottleneck, and it only saves a few hundred milliseconds at best. The majority of the actual rendering work happens between viewWillAppear and viewDidAppear, so you need to trigger a render if you want to avoid the lag during your initial transition.
To do this, you need to add your view controllers as child view controllers inside your main view. I found that the best place to do this was in the viewDidAppear callback on your main view controller, because this will not slow down the loading of your main view. And even though you're loading and adding the child views on the main thread, it doesn't seem to block the UI. (I've been testing on a view that has a background video playing.)
Here's the Swift code I've written to pre-render some view controllers:
enum ViewPreloadingState {
case Pending, Preloading, Loaded
}
var viewPreloadingState = ViewPreloadingState.Pending
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
if viewPreloadingState == .Pending {
viewPreloadingState = .Preloading
addChildViewController(loginViewController)
view.addSubview(loginViewController.view)
loginViewController.didMoveToParentViewController(self)
addChildViewController(signupViewController)
view.addSubview(signupViewController.view)
signupViewController.didMoveToParentViewController(self)
// Place view controllers off screen
let f = view.frame,
offScreenFrame = CGRect(x: f.width, y: 0, width: f.width, height: f.height)
loginViewController.view.frame = offScreenFrame
signupViewController.view.frame = offScreenFrame
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if viewPreloadingState == .Preloading {
viewPreloadingState = .Loaded
loginViewController.willMoveToParentViewController(nil)
loginViewController.view.removeFromSuperview()
loginViewController.removeFromParentViewController()
signupViewController.willMoveToParentViewController(nil)
signupViewController.view.removeFromSuperview()
signupViewController.removeFromParentViewController()
}
}

Related

ViewDidLoad method calling behavior [duplicate]

This question already has an answer here:
Method calling via performSelectorOnMainThread Vs Normal method calling
(1 answer)
Closed 9 years ago.
In my viewDidLoad, I call a function:
[common startActivityIndicatorOnView:self.view];
This method adds a view with Activity indicator, in the center of self.view.
My current view is pushed on a Navigation Stack. This is how the view looks after this method returns (the activity indicator view is not in center):
However, if I call the same method this way:
[common performSelectorOnMainThread:#selector(startActivityIndicatorOnView:) withObject:self.view waitUntilDone:NO];
The view looks like the following image (the activity indicator view is in center):
I do not get, How does it make a difference if the calling line is written in viewDidLoad.
If any one can help me get this, thanks in advance.
Just for reference,
the method looks like this:
-(void) startActivityIndicatorOnView:(UIView *)view {
if ([NSRunLoop currentRunLoop] != [NSRunLoop mainRunLoop]) {
[self performSelectorOnMainThread:#selector(startActivityIndicatorOnView:) withObject:view waitUntilDone:NO];
return;
}
view.userInteractionEnabled = NO;
activityBgView = [[UIView alloc] initWithFrame:CGRectMake((view.frame.size.width/2) - 50, (view.frame.size.width/2) - 50, 100, 100)];
activityBgView.center = view.center;
activityBgView.backgroundColor = [UIColor grayColor];
activityBgView.alpha = 0.8;
spinner = [[UIActivityIndicatorView alloc] initWithFrame:CGRectMake((activityBgView.frame.size.width/2)-10, (activityBgView.frame.size.width/2)-10, 20, 20)];
spinner.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhiteLarge;
spinner.center = view.center;
[view addSubview:activityBgView];
[view addSubview:spinner];
[spinner startAnimating];
}
ViewDidLoad is called when you load viewControllers (via nib/xib or created programmatically in the loadView method), meaning xcode create all views and instantiate and alloc all objects in xib...
but, your viewController view (and subViews) are still not added in any view...
yourViewController.view.superView = nil;
so, its view has got the frame that you set in xib, but if you tell it to resize inside its superview, when you add it (e.g. with a push or an addsubview), its frame changes, but your
spinner won't change its position.
calling a performSelectorOnMainThread just will call your method later, when your current thread step ahead and may have pushed your viewController.view, so, when executed, yourViewController.view.superView exists, and so view.frame has already changed.
try to move your call to
[common startActivityIndicatorOnView:self.view];
in a viewWillAppear method: at that point yourViewController.view should been already resized to fit its superView
EDIT:
# pavel question:
after what moment yourViewController.view.superView will be not nil?
banally: when you add its view to a view. that is, firts you allocate and init it (init with a nib or via code)
something like:
yourViewControllerInstance = [[YourViewController alloc]initWithNibName:#"yourViewControllerNib" bundle:nil];
at this point the method viewDidLoad in your class is called (but yourViewController.view.superview 0 nil)
later, you usually use your new viewController, so you "push" it in the stack of a navigationController, or you just add its view to the current viewController.view... something like:
[currentViewController.view addSubview:yourViewController.view];
after this line, as you may imagine, yourViewController.view.superView = currentViewController.view, and the method viewWillAppear of yourViewController is called, so you can use superView inside it.
Notice that at this point your viewController.view is still not visible on screen, so you can adjust sizes, move or add views here without users see any changes.
after this, yourViewController will show, and at the end, and the method viewDidAppear of yourViewController is called (for any other code, in case)

UIPopoverController for iphone not working?

I need to use a UIPopOverController for my iPhone app ,i searched stackoverflow someone said UIPopoverController does not run on iphone iphone device WHY?.when i run on iphone device
i got this error reason: '-[UIPopoverController initWithContentViewController:]
called when not running under UIUserInterfaceIdiomPad.'
-(void)btnSetRemainderTapped:(UIButton *)button
{
setReminderView =[[SetRemainderView alloc]initWithNibName:#"SetRemainderView" bundle:[NSBundle mainBundle]];
setReminderView.contentSizeForViewInPopover = CGSizeMake(setReminderView.view.frame.size.width, setReminderView.view.frame.size.height);
setReminderView.delegate = self;
popOverController = [[UIPopoverController alloc]
initWithContentViewController:setReminderView] ;
CGRect rect = CGRectMake(self.view.frame.size.width/2, self.view.frame.size.height/2, 1, 1);
[popOverController presentPopoverFromRect:rect
inView:self.view
permittedArrowDirections:UIPopoverArrowDirectionAny
animated:YES];
}
can any one help me?
You CAN use popoverController in iPhone apps.
1. Create a category
// UIPopoverController+iPhone.h file
#interface UIPopoverController (iPhone)
+ (BOOL)_popoversDisabled;
#end
// UIPopoverController+iPhone.m file
#implementation UIPopoverController (iPhone)
+ (BOOL)_popoversDisabled {
return NO;
}
#end
2. Import it to your class and use popover in iPhone as usual.
But remember that this is private method and Apple can reject your app. But I know people who use this normally and Apple published their apps.
Edit: As stated by Soberman, since iOS 8 it is possible to present popovers on iPhone using public APIs, so this answer is probably not relevant anymore.
As stated in Apple's documentation on UIPopoverController:
Popover controllers are for use exclusively on iPad devices.
So there is no way to use this class in iPhone application unfortunately. But there are a couple of custom third-party implementations of the functionality provided by UIPopoverController which add iPhone support and more. See https://github.com/50pixels/FPPopover for example.
Edit: There also is another highly customizable popover implementation for both iPhone/iPad worth checking out: https://github.com/nicolaschengdev/WYPopoverController.
Since iOS8 we are now able to create popovers, that will be the same on iPhone, as on iPad, which would be especially awesome for those who make universal apps, thus no need to make separate views or code.
You can get the class as well as demo project here: https://github.com/soberman/ARSPopover
All you need to do is subclass UIViewController, conform to the UIPopoverPresentationControllerDelegate protocol and set desired modalPresentationStyle along with the delegate value:
// This is your CustomPopoverController.m
#interface CustomPopoverController () <UIPopoverPresentationControllerDelegate>
#end
#implementation CustomPopoverController.m
- (instancetype)init {
if (self = [super init]) {
self.modalPresentationStyle = UIModalPresentationPopover;
self.popoverPresentationController.delegate = self;
}
return self;
}
- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller {
return UIModalPresentationNone; //You have to specify this particular value in order to make it work on iPhone.
}
Afterwards, instantiate your newly created subclass in the method from which you want to show it and assign two more values to sourceView and sourceRect. It looks like this:
CustomPopoverController *popoverController = [[CustomPopoverController alloc] init];
popoverController.popoverPresentationController.sourceView = sourceView; //The view containing the anchor rectangle for the popover.
popoverController.popoverPresentationController.sourceRect = CGRectMake(384, 40, 0, 0); //The rectangle in the specified view in which to anchor the popover.
[self presentViewController:popoverController animated:YES completion:nil];
And there you have it, nice, neat blurred popover.
So #Sobermans answer didn't really solve the issue from start to finish for me so I want to detail how I got it done using the docs. That being said I do like the idea of using your own presentation controller subclass to manage all of the customisation you want to exhibit.
1. Create your controller to present
The first step is instantiating the controller you want to present:
let vc: UIViewController = ...
vc.modalPresentationStyle = .Popover
vc.preferredContentSize = CGSize(width: CGRectGetWidth(view.bounds)/2, height: 100)
Now we have a controller with the popover presentation style and an arbitrary content size.
2. Implement adaptivePresentationStyleForPresentationController
By default UIPopoverPresentationController will present on full screen on iPhone so to prevent this behaviour you need to force the adaptive presentation style to none.
First we set the delegate of the popover presentation controller
vc.popoverPresentationController.delegate = self;
Then we implement UIPopoverPresentationControllerDelegate
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle {
return .None;
}
3. Present and configure popup
First we need to call presentViewController and only after that can we configure the popover:
presentViewController(vc, animated:true, completion:nil)
if let popover = vc.popoverPresentationController {
popover.permittedArrowDirections = .Right | .Left
popover.sourceView = button
popover.sourceRect = button.bounds
}
Use a custom popover controller, such as:
https://github.com/sammcewan/WYPopoverController
(this seems to be the best supported one that I have found).
I ended up creating my custom tooltip/popover class.
Can be initalised with any content view and dynamically adjusts it's frame.
Hope it helps.
https://github.com/akeara/AKETooltip
If you want to do it in Swift, I believe the code is the following:
extension UIPopoverController {
class var _popoversDisabled : Bool {
get { return false }
}
}
Edit: It is working in Xcode 6 beta 4 on iPhone with iOs7.1
This is a really interesting (and depressing) thread to read. I can't believe Apple prevents popup dialogs on iPhones, with absolutely no justification.
And, it's true, on iOS 8, if you try to work around this limitation, it'll make your popups appear as a full-screen modal dialog.
The following excellent webpage describes "How Apple Cheats" to let its own iBooks and iTunes apps break its own rules, and allow popups - but just from within their own iPhone apps.
HowAppleCheats
Have a read (warning: it'll make you hate Apple & XCode even more..)
Want to get around the "UIPopoverController called when not running under UIUserInterfaceIdiomPad" error on iOS 8 ?
Simple.
Just go into your .plist file, and change the Bundle ID to "com.apple.itunesu" to make XCode think that your app is actually iTunes.
Then your popup will work fine.
(Sigh.)
The alternative way of doing this is to directly add your UIViewController to your screen.
In this example, I wanted a "helper screen" to appear on top of my iPhone screen. It's a UIViewController, it is stored in it's own .xib file, and it has a few lines to add a pretty border:
- (void)viewDidLoad {
[super viewDidLoad];
// Give our popup a pretty curved border
self.view.layer.borderColor = [[UIColor blueColor] CGColor];
self.view.layer.borderWidth = 1.0;
self.view.layer.cornerRadius = 8;
}
To display it, I simply create an instance of this UIViewController, add it to my screen, then center it:
-(void)showHelperScreen
{
if (self.helperScreen == nil)
{
// Add the popup UIViewController to our screen
self.helperScreen = [[HelperViewController alloc] init];
[self.view addSubview:self.helperScreen.view];
}
// Center the popup in the middle of the screen
CGSize screenSize = [[UIScreen mainScreen] applicationFrame].size;
self.helperScreen.view.center = CGPointMake(screenSize.width/2, screenSize.height/2);
}
Of course, I also needed to add some code to make the popup disappear when the user taps outside of it, but this does at least show that you can (safely) display popups on an iPhone, even if your app isn't specifically called iTunes or iBook.
Voila.
Hope this helps, and if anyone needs me, I'll be back in my safe, happy place (Visual Studio, in other words).

SplitViewController Woes - replacing detail view controller in portrait mode failure

I've got a SVC setup which works well. The master and detail views of the SPC are both UINavigationControllers, with separate root view controllers each.
My application works perfectly. I can start it up in either portrait or landscape mode and everything works as it aught to. The 'master' popup button is visible in portrait mode but hidden in landscape mode. Both master and detail planes animate and rotate properly, there is no popping or weird animation artifacts present, and everything stretches properly. 100% satisfaction per spec.
Issues arise when I replace the detail view controller. Since we cannot replace the root node of a uinavi controller, what I do is create a brand new UINavigationController, assigning it the new view I want in the detail view. I do this because the views in the detail view can do 1-2-3 levels deep, but I need to be able to assign unique 'root' views (eg I need to be able to completely replace the root UINavigationController).
The code looks like this:
self.detailViewController = [[SomeNewController1of3 alloc] initWithNibName:#"SomeNewController1of3" bundle:nil];
self.splitViewController.delegate = self.detailViewController;
UINavigationController *newNC = [[UINavigationController alloc] initWithRootViewController:self.detailViewController];
self.splitViewController.viewControllers = #[navigationController, newN];
First, I create the new viewcontroller, whatever it may be. I then set the splitviewcontroller's delegate to = the newly created viewcontroller (so that willHideViewController, etc fires). Then I create the a navicontroller to house the newly created viewcontroller, so that it can push and pop from it. And finally, I replace the second (detail) view of the split view controller with my new navi controller.
Again, this plan works flawlessly when I start in landscape.
When I start in portrait, these are the anomalies I've noticed:
All popups (alert boxes, the master view, etc) have weird rotations and flip out from an awkward spot when they are shown. It's almost as if it rotates these popups right as they're displayed, instead of having them simply 'slide' out of their correct locations.
When the master view is displayed, after doing the weird rotation describes above, it disappears completely for an instant (leaving a black, empty region) then reanimates to it's original location.
Until I complete a device rotation, the 'master' button on the detail view controller which displays the popup for the master view is not shown. After confirming with breakpoints, I've indeed concluded that willHideViewController is Not being called by my above method.
What I don't get is why everything behaves flawlessly when I start in landscape, but all of these errors arise in portrait? All of my views actually have a default orientation set to portrait in IB, and are scale to fit. Also it's not like they're crazy interfaces, they are all just stock tableviews.
Have any of you encountered this issue or found a method around it?
Firstly, the UISplitViewController delegate should be a top level object like your App Delegate, not a view controller the split controller is showing.
Secondly, to answer your question, to replace a showing detail view controller (i.e. pushed on the master nav) in portrait you can make use of a split controller delegate method:
AppDelegate.m
- (BOOL)splitViewController:(UISplitViewController *)splitViewController showDetailViewController:(UIViewController *)vc sender:(id)sender{
if(!splitViewController.isCollapsed){
return NO;
}
UINavigationController *masterNav = splitViewController.viewControllers.firstObject;
if(![masterNav.topViewController isKindOfClass:UINavigationController.class]){
return NO;
}
UINavigationController *existingDetailNav = (UINavigationController *)masterNav.topViewController;
UINavigationController *newDetailNav = (UINavigationController *)vc;
existingDetailNav.viewControllers = #[newDetailNav.viewControllers.firstObject];
return YES;
}
You might be wondering why we don't just replace the master nav stack. Well the problem is the split view controller preserves the detail view controller from the last time showDetail that went through its default behavior. So if we were in portrait, tapped on a cell to show detail, then replaced the detail, then navigate back then upon rotating to landscape the old detail will show on the right and not the new one. By replacing the detail nav stack with the new detail controller as above this solves this because although the split is showing the preserved detail nav it now contains the new detail.
I find it is easier to subclass the SplitViewController. From there, you can easily swap out the the detail adding a navController for those that need it.
Subclass UISplitViewController and set your root splitViewController to that class. Then add this method to your UISplitViewController subclass:
-(void)setDetailControllerTo:(UIViewController *)detailController withNavControllerTitle:(NSString *)title {
[detailController view]; // this line forces the viewDidLoad method to be called
if (title) {
UINavigationController *navController = [[UINavigationController alloc] init];
[navController pushViewController:detailController animated:YES];
detailController.title = title;
NSArray *viewControllers=#[self.mainController.viewControllers[0],navController];
self.mainController.viewControllers = viewControllers;
} else {
NSArray *viewControllers=#[self.mainController.viewControllers[0],detailController];
self.mainController.viewControllers = viewControllers;
}
}
To call this method do something like this from the master view controller in the tableView:didSelectRowAtIndexPath: method
FixedSplitViewController *splitController = (FixedSplitViewController*) self.splitViewController;
CurrentEventViewController *controller = [self.storyboard instantiateViewControllerWithIdentifier:#"CurrentEventViewController"];
// add any setup code here
[splitController setDetailControllerTo:controller withNavControllerTitle:#"Current Event"];
If you wish to keep the master view visible in portrait rotation, add this method to the SplitViewController subclass:
-(BOOL)splitViewController:(UISplitViewController *)svc shouldHideViewController:(UIViewController *)vc inOrientation:(UIInterfaceOrientation)orientation {
return NO;
}

One ViewController vs. many ViewControllers

I'm building an app which consists of different views which are closely related to each other. So far, I only have one UIViewController which controls these different views. View 1 and 2 share the same background, for instance, and the transition between view 1 and 2 is a custom animation.
My problem is that both view 1 and 2 have an UIScrollView. My UIViewController is their delegate and I could have the following scrollViewDidScroll to distinguish between the two scrollviews:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView.tag == 1)
//handle a
else if (scrollView.tag == 2)
//handle b
else if (scrollView.tag == 3)
//handle c
}
As a lot happens with scrollView 1 and different things happen with scrollView2, the code will become really messy. Ideally, I'd like to define in a separate file what happens if scrollView1 is scrolled etc.. Yet I don't want to have another UIViewController as then transitions become more difficult. I don't have a NavBar or ToolBar, so neither UINavigationController nor UITabBarController would work very well in my case.
What should I do?
I posted a similar question here.
If you don't want two view controllers, just create a separate delegate for each scroll view. Make it an NSObject which conforms to UIScrollViewDelegate and create it at the same time as the scroll view.
Seems to combine the results you seek: one view controller, but encapsulated scroll view code.
You could have a base controller class that handles the common functionality. Each different controller can inherit from this and override with their specific functionality as required.
Aka the template pattern
Edit
To expand. You say you want only one view controller. So you should create a separate class to handle the individual functionality. The View Controller has a base class pointer which gets swapped around according to the current view.
In pseudo code :
class BaseFunctionality
-(void) handleDidScroll {}
end
class ScrollViewAFunctionality : BaseFunctionality
-(void) handleDidScroll {
// Lots of interesting technical stuff...
}
end
class ScrollViewBFunctionality : BaseFunctionality
-(void) handleDidScroll {
// Lots of interesting technical stuff...
}
end
class TheViewController : UIViewController
BaseFunctionality *functionality;
-(void) swapViews {
// Code to swap views
[this.functionality release];
if (view == A)
this.functionality = [[ScrollViewAFunctionality alloc] init]
else if ( view == B)
this.functionality = [[ScrollViewBFunctionality alloc] init]
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
[this.functionality handleDidScroll];
}
end

initWith Frame?? CGRectMake - XIB Size is wrong

In my MainControllerClass I have a few XIB's that I am initializing and throwing on the screen - Here is an Example
self.widgetPeopleToBuyFor = [[WidgetAddPeopleToBuyFor alloc] initWithNibName:#"WidgetAddPeopleToBuyFor" bundle:nil andUser:self.currentUser];
self.widgetPeopleToBuyFor.view.frame = CGRectMake(10,10,492,537);
self.widgetPeopleToBuyFor.delegate = self;
[self.viewMainView addSubview:self.widgetPeopleToBuyFor.view];
[self.viewMainView bringSubviewToFront:self.widgetPeopleToBuyFor.view];
if ([self.currentUser.wPeopleToBuyForRect length] > 2) {
NSLog(#"LOADING FROM DB");
CGRect rect9 = CGRectFromString(self.currentUser.wPeopleToBuyForRect);
self.widgetPeopleToBuyFor.view.frame = rect9;
}
else
{
NSLog(#"FIRST TIME");
self.widgetPeopleToBuyFor.view.frame = CGRectMake(10,10,492,537);
}
I'm building this app so the user can resize the the view to their desire, move it around etc. So when he user Exits I call this method to grab the view information.
- (NSString *) showInfoViewSize: (UIView *) view
{
return NSStringFromCGRect(view.frame);
}
So from there I save the information in the Database, it saves ok - But I just learned the size doesn't matter - I could change it to (10,10,200,200) and the view stays the same exact size on the screen.
So I know I threw a buch at you there. But in the End, the DB works fine - the IF statement gets fired off - Its just I can't resize the XIB on launch.
Should I be resizing it a different way? Should I keep track of the TRANSFORM from the pinch recoginier and just retransform it the scaleFactor at load? Seems kinda hokey to me.
Any help is appreciated - I've been beating myself up on this, and I bet its something super simple I'm missing.
view does not loads from xib until it is presented and hence at that time there will be no frame to set for view. let the view load first then set its frame afterwards.
change the view.frame in "viewWillAppear" in your controller's implementation.
Normally when something is just not working, it's because you've got a nil floating around somewhere you didn't expect. Remember, sending messages to nil is completely legal in ObjC (it just always returns nil).
So I'd step through this in the debugger and check everything to make sure it's a real instance and not nil.
But generally what you're doing (allocing a controller with a nib, setting that controller's view's frame, then adding the controller's view to a superview) is the correct way to do it.