UISegmentedControl as UINavigationBar title item loads after some delay - ios

Since I updated to Xcode8/iOS10 & Swift 3 I have this weird issue on segmented controls used as navigation bar titles.. it takes some time to appear. Or I can touch the area (invisible at that time) and it will appear.
I believe the item as been loaded but just not be drawn and then it got either draw by touching the area or because of the event loop asking for view redraw.
UI is not blocked.
can't find a way to fix it.
PS: could not reproduce on fresh new project
Breaking with Debug View Hierarchy, it appears the UISegmentedControls lives but is not initialised with colors/texts.. then it will, few seconds later.

Ok this was due to threads.
My app on loading was initialising a core data stack. On the callback of it I was instantiating the right view controller using window.rootViewController = vc, this needs to be executed on the main thread, but the completion of the core data stack init was launch on a background one.
Something weird yet. I had implemented a UIWindow extension
extension UIWindow {
func setRootViewController(with viewController: UIViewController) {
DispatchQueue.main.async {
self.rootViewController = viewController
}
}
}
// beeing in background thread
self.window.setRootViewController(with: self.rootVC)
this will produce the issue below.
but the next will work
extension UIWindow {
func setRootViewController(with viewController: UIViewController) {
self.rootViewController = viewController
}
}
// beeing in background thread
DispatchQueue.main.async {
self.window.setRootViewController(with: self.rootVC)
}
don't know what's the difference.

Related

Why my alert is not in the view hierarchy

I found that my controller is out of the view hierarchy in this code andI call this in viewDidLoad:
if CLLocationManager.locationServicesEnabled() {
// code
} else {
let alertController = UIAlertController(...)
//
present(alertController)
}
If I wrap else clause in .async or .asyncAfter in the main queue I have my issue go away.
Why do this happen here?
Thank you!
based on your question you are tried to load UIAlertController before load the UIviewcontroller hierarchy, in here you can do it two ways,
you can forcefully load the UIAlertController in the main thread, so in here you need to use .async or .asyncAfter, but it's not suggested.
alternate suggestion but it will works fine, you need to wait for your initial UI view hierarchy, I mean you need to convert your code from viewDidLoad to viewDidAppear. for ref : Difference between viewDidLoad and viewDidAppear

Run Code Before First View Controller is Initialized (Storyboard-based App)

My app needs to perform some cleanup tasks -delete old data stored locally- every time on startup, before the initial view controller is displayed.
This is because that initial view controller loads the existing data when initialized, and displays it inside a table view.
I set up several breakpoints and found out that the initial view controller's initializer (init?(coder aDecoder: NSCoder)) is run before application(_:didFinishLaunchingWithOptions) - even before application(_:**will**FinishLaunchingWithOptions), to be precise.
I could place the cleanup code at the very top of the view controller's initializer, but I want to decouple the cleanup logic from any particular screen.
That view controller may end up not being the initial view controller one day.
Overriding the app delegate's init() method and putting my cleanup code there does get the job done, but...
Question:
Isn't there a better / more elegant way?
Note: For reference, the execution order of the relevant methods is as
follows:
AppDelegate.init()
ViewController.init()
AppDelegate.application(_:willFinishLaunchingWithOptions:)
AppDelegate.application(_:didFinishLaunchingWithOptions:)
ViewController.viewDidLoad()
Clarification:
The cleanup task is not lengthy and does not need to run asynchronously: on the contrary, I'd prefer if my initial view controller isn't even instantiated until it completes (I am aware that the system will kill my app if it takes to long to launch. However, it's just deleting some local files, no big deal.).
I am asking this question mainly because, before the days of storyboards, app launch code looked like this:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.window.backgroundColor = [UIColor whiteColor];
// INITIAL VIEW CONTROLLER GETS INSTANTIATED HERE
// (NOT BEFORE):
MyViewController* controller = [[MyViewController alloc] init];
self.window.rootViewController = controller;
[self.window makeKeyAndVisible];
return YES;
}
...but now, because main storyboard loading happens behind the scenes, the view controller is initialized before any of the app delegate methods run.
I am not looking for a solution that requires to add custom code to my initial view controller.
I am not sure if there is more elegant way but there are definitely some other ways...
I'd prefer if my initial view
controller isn't even instantiated until it completes
This is not a problem. All you have to do is to delete a UIMainStoryboardFile or NSMainNibFile key from the Info.plist which tells the UIApplicationMain what UI should be loaded. Subsequently you run your "cleanup logic" in the AppDelegate and once you are done you initiate the UI yourself as you already shown in the example.
Alternative solution would be to create a subclass of UIApplicationMain and run the cleanup in there before the UI is loaded.
Please see App Life Cycle below:
You can add a UIImageView on your initial ViewController which will contain the splash image of your app.
In viewDidLoad()... make the imageview.hidden property False... do you cleanup operation and on completion of cleanup task make the imageview.hidden property TRUE.
By this user will be unaware of what job you are doing and this approach is used in many recognized app.
I faced a very simmilar situation
where i needed to run code, which was only ready after didFinishLaunchingNotification
i came up with this pattern which also works with state restoration
var finishInitToken: Any?
required init?(coder: NSCoder) {
super.init(coder: coder)
finishInitToken = NotificationCenter.default.addObserver(forName: UIApplication.didFinishLaunchingNotification, object: nil, queue: .main) { [weak self] (_) in
self?.finishInitToken = nil
self?.finishInit()
}
}
func finishInit() {
...
}
override func decodeRestorableState(with coder: NSCoder) {
// This is very important, so the finishInit() that is intended for "clean start"
// is not called
if let t = finishInitToken {
NotificationCenter.default.removeObserver(t)
}
finishInitToken = nil
}
alt you can make a notif for willFinishLaunchingWithOptions
Alternatively, if you want a piece of code to be run before everything, you can override the AppDelegate's init() like this:
#main
class AppDelegate: UIResponder, UIApplicationDelegate {
override init() {
DoSomethingBeforeEverthing() // You code goes here
super.init()
}
...
}
Few points to remember:
You might want to check, what's allowed/can be done here, i.e. before app delegate is initialized
Also cleaner way would be, subclass the AppDelegate, and add new delegate method that you call from init, say applicationWillInitialize and if needed applicationDidInitialize

iOS 7, corrupt UINavigationBar when swiping back fast using the default interactivePopGestureRecognizer

I have an issue that I'm stuck on, but I have no idea why it even happens; If I push a detail controller on the stack, and I swipe back very quickly using the default left edge interactivePopGestureRecognizer, my parent/root view controller's UINavigationBar looks corrupt or something, almost like the built in iOS transition mechanism didn't have time to do it's job at resetting it after the detail view is gone. Also to clarify, everything in this 'corrupt' UINavigationBar is still touchable and everything on my parent/root view controller works perfectly.
For people downvoting due to no source code: there is no source code! This is an Apple bug!
Is there anyway to reset this UINavigationBar to what it should be when the parent/root view controller's viewDidAppear method gets called?
Note that this bug does not occur if I tap the top left back button instead of using the left edge interactivePopGestureRecognizer.
Edit: I added an NSLog to check the navigationBar's subview count on viewDidAppear on the parent/root view controller, and the count is always the same, corrupt or not, so I'd like to know why the popped controller is wreaking havoc with my UINavigationBar.
If you can help me at all, I'd greatly appreciate it! Thank you.
I've attached a screenshot of what it looks like: Note that the back chevron isn't part of my parent/root view controller, it's part of what was popped off the stack. Testing123 is the title for the parent/root view controller and not that of what was popped off the stack. The head and gear icons are part of the parent/root view controller.
Edit: I've thought something like this could fix the issue, but turns out it doesn't, and is really bad experience IMO too. This is not the kind of solution I'm looking for. I'm posting a large bounty so this can be resolved correctly! 😃. I just can't have this weird UI behavior be in a production quality app.
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self.navigationController pushViewController:[UIViewController new] animated:NO];
[self.navigationController popToRootViewControllerAnimated:YES];
}
TL;DR
I made a category on UIViewController that hopefully fixes this issue for you. I can't actually reproduce the navigation bar corruption on a device, but I can do it on the simulator pretty frequently, and this category solves the problem for me. Hopefully it also solves it for you on the device.
The Problem, and the Solution
I actually don't know exactly what causes this, but the navigation bar's subviews' layers' animations seem to either be executing twice or not fully completing or... something. Anyway, I found that you can simply add some animations to these subviews in order to force them back to where they should be (with the right opacity, color, etc). The trick is to use your view controller's transitionCoordinator object and hook into a couple of events – namely the event that happens when you lift your finger up and the interactive pop gesture recognizer finishes and the rest of the animation starts, and then the event that occurs when the non-interactive half of the animation finishes.
You can hook into these events using a couple methods on the transitionCoordinator, specifically notifyWhenInteractionEndsUsingBlock: and animateAlongsideTransition:completion:. In the former, we create copies of all of the current animations of the navbar's subviews' layers, modify them slightly, and save them so we can apply them later when the non-interactive portion of the animation finishes, which is in the completion block of the latter of those two methods.
Summary
Listen for when the interactive portion of the transition ends
Gather up the animations for all the views' layers in the navigation bar
Copy and modify these animations slightly (set fromValue to the same thing as the toValue, set duration to zero, and a few other things)
Listen for when the non-interactive portion of the transition ends
Apply the copied/modified animations back to the views' layers
Code
And here's the code for the UIViewController category:
#interface UIViewController (FixNavigationBarCorruption)
- (void)fixNavigationBarCorruption;
#end
#implementation UIViewController (FixNavigationBarCorruption)
/**
* Fixes a problem where the navigation bar sometimes becomes corrupt
* when transitioning using an interactive transition.
*
* Call this method in your view controller's viewWillAppear: method
*/
- (void)fixNavigationBarCorruption
{
// Get our transition coordinator
id<UIViewControllerTransitionCoordinator> coordinator = self.transitionCoordinator;
// If we have a transition coordinator and it was initially interactive when it started,
// we can attempt to fix the issue with the nav bar corruption.
if ([coordinator initiallyInteractive]) {
// Use a map table so we can map from each view to its animations
NSMapTable *mapTable = [[NSMapTable alloc] initWithKeyOptions:NSMapTableStrongMemory
valueOptions:NSMapTableStrongMemory
capacity:0];
// This gets run when your finger lifts up while dragging with the interactivePopGestureRecognizer
[coordinator notifyWhenInteractionEndsUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> context) {
// Loop through our nav controller's nav bar's subviews
for (UIView *view in self.navigationController.navigationBar.subviews) {
NSArray *animationKeys = view.layer.animationKeys;
NSMutableArray *anims = [NSMutableArray array];
// Gather this view's animations
for (NSString *animationKey in animationKeys) {
CABasicAnimation *anim = (id)[view.layer animationForKey:animationKey];
// In case any other kind of animation somehow gets added to this view, don't bother with it
if ([anim isKindOfClass:[CABasicAnimation class]]) {
// Make a pseudo-hard copy of each animation.
// We have to make a copy because we cannot modify an existing animation.
CABasicAnimation *animCopy = [CABasicAnimation animationWithKeyPath:anim.keyPath];
// CABasicAnimation properties
// Make sure fromValue and toValue are the same, and that they are equal to the layer's final resting value
animCopy.fromValue = [view.layer valueForKeyPath:anim.keyPath];
animCopy.toValue = [view.layer valueForKeyPath:anim.keyPath];
animCopy.byValue = anim.byValue;
// CAPropertyAnimation properties
animCopy.additive = anim.additive;
animCopy.cumulative = anim.cumulative;
animCopy.valueFunction = anim.valueFunction;
// CAAnimation properties
animCopy.timingFunction = anim.timingFunction;
animCopy.delegate = anim.delegate;
animCopy.removedOnCompletion = anim.removedOnCompletion;
// CAMediaTiming properties
animCopy.speed = anim.speed;
animCopy.repeatCount = anim.repeatCount;
animCopy.repeatDuration = anim.repeatDuration;
animCopy.autoreverses = anim.autoreverses;
animCopy.fillMode = anim.fillMode;
// We want our new animations to be instantaneous, so set the duration to zero.
// Also set both the begin time and time offset to 0.
animCopy.duration = 0;
animCopy.beginTime = 0;
animCopy.timeOffset = 0;
[anims addObject:animCopy];
}
}
// Associate the gathered animations with each respective view
[mapTable setObject:anims forKey:view];
}
}];
// The completion block here gets run after the view controller transition animation completes (or fails)
[coordinator animateAlongsideTransition:nil completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
// Iterate over the mapTable's keys (views)
for (UIView *view in mapTable.keyEnumerator) {
// Get the modified animations for this view that we made when the interactive portion of the transition finished
NSArray *anims = [mapTable objectForKey:view];
// ... and add them back to the view's layer
for (CABasicAnimation *anim in anims) {
[view.layer addAnimation:anim forKey:anim.keyPath];
}
}
}];
}
}
#end
And then just call this method in your view controller's viewWillAppear: method (in your test project's case, it would be the ViewController class):
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self fixNavigationBarCorruption];
}
After investigating this issue for some time with debug console, Instruments and Reveal, I have found out the following:
1) On simulator the bug can be recreated every time, if using Profile/Automation Template and adding the following script:
var target = UIATarget.localTarget();
var appWindow = target.frontMostApp().mainWindow();
appWindow.buttons()[0].tap();
target.delay(1);
target.flickFromTo({x:2, y: 100}, {x:160, y: 100});
2) On real device (iPhone 5s, iOS 7.1) this script never causes the bug. I tried various options for flick coordinates and the delay.
3) UINavigationBar consists of:
_UINavigationBarBackground (doesn't seem to be related to the bug)
_UIBackdropView
_UIBackgropEffectView
UIView
UIImageView
UINavigationItemView
UILabel (visible in the bug)
_UINavigationBarBackIndicatorView (visible in the bug)
4) When bug happens UILabel looks half transparent and in the wrong position, but the actual properties of the UILabel are correct (alpha: 1 and frame as in normal situation). Also _UINavigationBarBackIndicatorView looks doesn't correspond to actual properties - it is visible although it's alpha is 0.
From this I conclude that it's a bug of Simulator and that you can't even detect from the code that something is wrong.
So #troop231 - are you 100% sure this also happens on device?
Key Concept
Disable gesture recognizer when pushing view controller, and enable it when view appeared.
A Common Solution: Subclassing
You can subclass UINavigationController and UIViewController to prevent corruption.
MyNavigationController : UINavigationController
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
[super pushViewController:viewController animated:animated];
self.interactivePopGestureRecognizer.enabled = NO; // disable
}
MyViewController : UIViewController
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
self.navigationController.interactivePopGestureRecognizer.enabled = YES; // enable
}
Problem: Too annoying
Need to use MyNavigationController and MyViewController instead of UINavigationController and UIViewController.
Need to subclass for UITableViewController, UICollectionViewController, and more.
A Better Solution: Method Swizzling
It could be done by swizzling UINavigationController and UIViewController methods. Want to know about method swizzling, visit here.
Example below uses JRSwizzle that makes method swizzling easy.
UINavigationController
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self jr_swizzleMethod:#selector(viewDidLoad)
withMethod:#selector(hack_viewDidLoad)
error:nil];
[self jr_swizzleMethod:#selector(pushViewController:animated:)
withMethod:#selector(hack_pushViewController:animated:)
error:nil];
});
}
- (void)hack_viewDidLoad
{
[self hack_viewDidLoad];
self.interactivePopGestureRecognizer.delegate = (id<UIGestureRecognizerDelegate>)self;
}
- (void)hack_pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
[self hack_pushViewController:viewController animated:animated];
self.interactivePopGestureRecognizer.enabled = NO;
}
UIViewController
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self jr_swizzleMethod:#selector(viewDidAppear:)
withMethod:#selector(hack_viewDidAppear:)
error:nil];
});
}
- (void)hack_viewDidAppear:(BOOL)animated
{
[self hack_viewDidAppear:animated];
self.navigationController.interactivePopGestureRecognizer.enabled = YES;
}
Being Simple: Use Open Source
SwipeBack
SwipeBack does it automatically without any code.
With CocoaPods, just add a line below into your Podfile. You don't need to write any code. CocoaPods automatically import SwipeBack globally.
pod 'SwipeBack'
Install pod, and it's done!

How to know which popover is being dismissed

I have a viewcontroller that can show several popovers. Not at the same time. Which would be the best way to know which popover is being dismissed at popoverControllerDidDismissPopover?
I have to do different actions regarding the popover that is being dismissed.
Thanks a lot
Something like this should work. (This code is not complete - I assume you know basic memory and class management and other stuff so I focus on the actual problem)
In your class keep some ivars to store reference to the popovercontrollers you created
#interface MyClass : NSObject <UIPopoverControllerDelegate> {
UIPopoverController *popover1;
UIPopoverComtroller *popover2;
}
Init your popovercontrollers as usual and save a referance to each of them in popover1 and popover2.
Then in your implementation of the UIPopoverDelegate protocol:
- (void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController {
if(popoverController == popover1) {
//popover1 was dismissed
} else if (popoverController == popover2) {
//popover2 was dismissed
}
}
EDIT: Looking at your comments, it seems that you mean that you are using only ONE popovercontroller, and replacing it's content view with different UIViewControllers.
If this is the case, I suggest you perform whatever the actions are inside those particular UIViewController in such a way that it affects your application´s state.
Then, once the popover is dismissed, you reload your original view with the new refreshed state.
Or you change it to use two different instances of UIPopoverController instead.
This is how in Swift as of Xcode 6.3 beta 3, should be similar in Objective-C.
Your presented view should have a ViewController for itself.
import UIKit
class MenuBookmarksViewController: UITableViewController {
}
Add an extension to the UIViewController class or place the code (below) inside the UIViewController that will be presenting your popovers:
extension UIViewController: UIPopoverPresentationControllerDelegate {
public func popoverPresentationControllerDidDismissPopover(popoverPresentationController: UIPopoverPresentationController) {
if popoverPresentationController.presentedViewController as? MenuBookmarksViewController != nil {
///do your stuff
}
}
}
You are passed which popover is being dismissed in popoverControllerDidDismissPopover:. Use that to determine what you want to do in each case.
You'll probably want to store your UIPopoverController instances as ivars of whatever object is presenting them, and then just compare against the value that you're passed in the delegate method.

Return to root view in IOS

To some this may sound like a daft question. I've searched around, and found little, mostly because I cannot find the appropriate search terms.
Here what I want to do is:
The application begins at view A.
View A launches view B, and view B launches view C.
Is their a way for view C to return directly back to A without dismissing itself and thus exposing B. For example a main menu button.
You can call popToRootViewControllerAnimated: if you have a UINavigationController. If you specify NO to animate it, then it will just jump back to the root without showing B first.
I have discovered a solution to my problem. Its a bit dirty, (and I''ll probably get shot down in flames for it) but works very well under tests and is very quick to implement. Here's how I did it.
In my app I have a Singleton class called GlobalVars (I use this for storing various global settings). This class holds a boolean called home_pressed and associated accessors (via synthesise). You could also store this value in the application delegate if you wish.
In every view controller with a main menu button, I wire the button to the homePressed IBAction method as follows. First setting the global homePressed boolean to YES, then dismissing the view controller in the usual way, but with NO animation.
-(IBAction) homePressed: (id) sender
{
[GlobalVars _instance].homePressed = YES;
[self dismissModalViewControllerAnimated: NO];
}//end homePressed
In every view controller except the main menu I implement the viewDidAppear method (which gets called when a view re-appears) as follows.
-(void) viewDidAppear: (Bool) animated
{
if ([GlobalVars _instance].homePressed == YES)
{
[self dismissModalViewController: NO];
}
else
{
//put normal view did appear code here/
}
}//end viewDidAppead
In the mainMenu view controller which is the root of the app, I set the global homePressed boolean to NO in its view did appear method as follows
-(void) viewDidAppear: (Bool) animated
{
if ([GlobalVars _instance].homePressed == YES)
{
[GlobalVars _instance].homePressed == NO;
}
else
{
//put normal view did appear code here/
}
}//end viewDidAppear
There, this enables me to go back to the root main menu of my app from any view further down the chain.
I was hoping to avoid this method, but its better than re-implementing my app which is what I'd have to do if I wanted use the UINavigationController solution.
Simple, took me 10 minutes to code in my 9 view app. :)
One final question I do have, would my solution be OK with the HIG?

Resources