Swift: Visual Glitches When Presenting a Main and Alternative (Login/Onboarding) Flow

How to present a splash/login view controller using storyboards

EDIT, July 2017
Since the first writing, I've changed my practice to one where I give the start up tasks to their own view controller. In that VC, I check startup conditions, present a "busy" UI if needed, etc. Based on the state at startup, I set the window's root VC.

In addition to solving the OP problem, this approach has the additional benefits of giving better control of the startup UI and UI transitions. Here's how to do it:

In your main storyboard, add a new VC, called LaunchViewController and make it the app's initial vc. Give your app's "real" initial vc an identifier like "AppUI" (identifiers are on the Identity tab in IB).

Identify other vcs that are the starts of main UI flows (e.g. Signup/Login, Tutorial, and so on) and give these descriptive identifiers, too. (Some prefer to keep each flow in it's own storyboard. That's a good practice, IMO).

One other nice optional idea: give your app's launch storyboard's vc an identifier, too (like "LaunchVC"), so that you can grab it and use it's view during startup. This will provide a seamless experience for the user during launch and while you do your startup tasks.

Here's what my LaunchViewController looks like....

@implementation LaunchViewController

- (void)viewDidLoad {
[super viewDidLoad];

// optional, but I really like this:
// cover my view with my launch screen's view for a seamless start
UIStoryboard *storyboard = [self.class storyboardWithKey:@"UILaunchStoryboardName"];
UINavigationController *vc = [storyboard instantiateViewControllerWithIdentifier:@"LaunchVC"];
[self.view addSubview:vc.view];
}

- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self hideBusyUI];
}

- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self showBusyUI];

// start your startup logic here:
// let's say you need to do a network transaction...
//
[someNetworkCallingObject doSomeNetworkCallCompletion:^(id result, NSError *error) {
if (/* some condition */) [self.class presentUI:@"AppUI"];
else if (/* some condition */) [self.class presentUI:@"LoginUI"];
// etc.
}];
}

#pragma mark - Busy UI

// optional, but maybe you want a spinner or something while getting started
- (void)showBusyUI {
// in one app, I add a spinner on my launch storyboard vc
// give it a tag, and give the logo image a tag, too
// here in animation, I fade out the logo and fade in a spinner
UIImageView *logo = (UIImageView *)[self.view viewWithTag:32];
UIActivityIndicatorView *aiv = (UIActivityIndicatorView *)[self.view viewWithTag:33];
[UIView animateWithDuration:0.5 animations:^{
logo.alpha = 0.0;
aiv.alpha = 1.0;
}];
}

- (void)hideBusyUI {
// an animation that reverses the showBusyUI
}

#pragma mark - Present UI

+ (void)presentUI:(NSString *)identifier {
UIStoryboard *storyboard = [self storyboardWithKey:@"UIMainStoryboardFile"];
UINavigationController *vc = [storyboard instantiateViewControllerWithIdentifier:identifier];

UIWindow *window = [UIApplication sharedApplication].delegate.window;
window.rootViewController = vc;

// another bonus of this approach: any VC transition you like to
// any of the app's main flows
[UIView transitionWithView:window
duration:0.3
options:UIViewAnimationOptionTransitionCrossDissolve
animations:nil
completion:nil];
}

+ (UIStoryboard *)storyboardWithKey:(NSString *)key {
NSBundle *bundle = [NSBundle mainBundle];
NSString *storyboardName = [bundle objectForInfoDictionaryKey:key];
return [UIStoryboard storyboardWithName:storyboardName bundle:bundle];
}

@end

The original answer below, though I prefer my current approach

Let's express the app's readiness to run the main vc with a boolean, something like:

BOOL readyToRun = startupWorkIsDone && userIsLoggedIn;
  1. Create an AppStartupViewController and lay it out in app storyboard.
  2. Don't drag any segue's to it, and don't make it the staring vc, just leave it floating somewhere.
  3. In the vc's attributes inspector in storyboard, set it's identifier as "AppStartupViewController".

In the AppStartupViewController.m, when the readyToRun conditions have been met, it can dismiss itself:

self.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;   // your choice here from UIModalTransitionStyle
[self dismissViewControllerAnimated:YES completion:nil];

Now, whenever the app becomes active, it can check for readiness to run, and present the AppStartupViewController if it's needed.
In AppDelegate.h

- (void)applicationDidBecomeActive:(UIApplication *)application {

BOOL readyToRun = startupWorkIsDone && userIsLoggedIn;

if (!readyToRun) {
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"MainStoryboard" bundle:nil];
AppStartupViewController *startupVC = [storyboard instantiateViewControllerWithIdentifier:@"AppStartupViewController"];

[self.window.rootViewController presentViewController:startupVC animated:NO completion:nil];
// animate = NO because we don't want to see the mainVC's view
}
}

That's mostly the answer, but there is one hitch. Unfortunately the main vc gets loaded (that's okay) and gets a viewWillAppear: message (not okay) before the AppStartupViewController is presented. It means we have to spread a little extra startup logic, like this, in MainViewController.m:

- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];

if (readyToRun) {
// the view will appear stuff i would have done unconditionally before
}
}

I hope this is helpful.

Swift default AlertViewController breaking constraints

This error is not critical, seems to be unfixed bug form Apple. This constraint appears in animation style just after presenting. Sample Image I tried to catch and change it (change values, relations, priority) before presenting – no success because of this dynamically added constraints.

When you turn off animation in self.present(alert, animated: false) and using alert.view.addSubview(UIView()) – the error disappears. I can't explain it, but it works!

let alert = UIAlertController(title: "Change your profile image", message: nil, preferredStyle: .actionSheet)

alert.addAction(UIAlertAction(title: "Photo Library", style: .default, handler: nil))
alert.addAction(UIAlertAction(title: "Online Stock Library", style: .default, handler: nil))
let cancel = UIAlertAction(title: "Cancel", style: .destructive, handler: nil)

alert.addAction(cancel)
alert.view.addSubview(UIView()) // I can't explain it, but it works!

self.present(alert, animated: false)

Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy?

You added constraint before you add your view to view hierarchy. Thas is why you get this error.
Be sure that turn by turn adding, for example :

view.addSubview(tableView) // 1
// 2 and start adding constraints..
tableView.translatesAutoresizingMaskIntoConstraints = false
...

How to use Auto Layout to move other views when a view is hidden?

It is possible, but you'll have to do a little extra work. There are a couple conceptual things to get out of the way first:

  • Hidden views, even though they don't draw, still participate in Auto Layout and usually retain their frames, leaving other related views in their places.
  • When removing a view from its superview, all related constraints are also removed from that view hierarchy.

In your case, this likely means:

  • If you set your left view to be hidden, the labels stay in place, since that left view is still taking up space (even though it's not visible).
  • If you remove your left view, your labels will probably be left ambiguously constrained, since you no longer have constraints for your labels' left edges.

What you need to do is judiciously over-constrain your labels. Leave your existing constraints (10pts space to the other view) alone, but add another constraint: make your labels' left edges 10pts away from their superview's left edge with a non-required priority (the default high priority will probably work well).

Then, when you want them to move left, remove the left view altogether. The mandatory 10pt constraint to the left view will disappear along with the view it relates to, and you'll be left with just a high-priority constraint that the labels be 10pts away from their superview. On the next layout pass, this should cause them to expand left until they fill the width of the superview but for your spacing around the edges.

One important caveat: if you ever want your left view back in the picture, not only do you have to add it back into the view hierarchy, but you also have to reestablish all its constraints at the same time. This means you need a way to put your 10pt spacing constraint between the view and its labels back whenever that view is shown again.

What are the Dangers of Method Swizzling in Objective-C?

I think this is a really great question, and it's a shame that rather than tackling the real question, most answers have skirted the issue and simply said not to use swizzling.

Using method sizzling is like using sharp knives in the kitchen. Some people are scared of sharp knives because they think they'll cut themselves badly, but the truth is that sharp knives are safer.

Method swizzling can be used to write better, more efficient, more maintainable code. It can also be abused and lead to horrible bugs.

Background

As with all design patterns, if we are fully aware of the consequences of the pattern, we are able to make more informed decisions about whether or not to use it. Singletons are a good example of something that's pretty controversial, and for good reason — they're really hard to implement properly. Many people still choose to use singletons, though. The same can be said about swizzling. You should form your own opinion once you fully understand both the good and the bad.

Discussion

Here are some of the pitfalls of method swizzling:

  • Method swizzling is not atomic
  • Changes behavior of un-owned code
  • Possible naming conflicts
  • Swizzling changes the method's arguments
  • The order of swizzles matters
  • Difficult to understand (looks recursive)
  • Difficult to debug

These points are all valid, and in addressing them we can improve both our understanding of method swizzling as well as the methodology used to achieve the result. I'll take each one at a time.

Method swizzling is not atomic

I have yet to see an implementation of method swizzling that is safe to use concurrently1. This is actually not a problem in 95% of cases that you'd want to use method swizzling. Usually, you simply want to replace the implementation of a method, and you want that implementation to be used for the entire lifetime of your program. This means that you should do your method swizzling in +(void)load. The load class method is executed serially at the start of your application. You won't have any issues with concurrency if you do your swizzling here. If you were to swizzle in +(void)initialize, however, you could end up with a race condition in your swizzling implementation and the runtime could end up in a weird state.

Changes behavior of un-owned code

This is an issue with swizzling, but it's kind of the whole point. The goal is to be able to change that code. The reason that people point this out as being a big deal is because you're not just changing things for the one instance of NSButton that you want to change things for, but instead for all NSButton instances in your application. For this reason, you should be cautious when you swizzle, but you don't need to avoid it altogether.

Think of it this way... if you override a method in a class and you don't call the super class method, you may cause problems to arise. In most cases, the super class is expecting that method to be called (unless documented otherwise). If you apply this same thought to swizzling, you've covered most issues. Always call the original implementation. If you don't, you're probably changing too much to be safe.

Possible naming conflicts

Naming conflicts are an issue all throughout Cocoa. We frequently prefix class names and method names in categories. Unfortunately, naming conflicts are a plague in our language. In the case of swizzling, though, they don't have to be. We just need to change the way that we think about method swizzling slightly. Most swizzling is done like this:

@interface NSView : NSObject
- (void)setFrame:(NSRect)frame;
@end

@implementation NSView (MyViewAdditions)

- (void)my_setFrame:(NSRect)frame {
// do custom work
[self my_setFrame:frame];
}

+ (void)load {
[self swizzle:@selector(setFrame:) with:@selector(my_setFrame:)];
}

@end

This works just fine, but what would happen if my_setFrame: was defined somewhere else? This problem isn't unique to swizzling, but we can work around it anyway. The workaround has an added benefit of addressing other pitfalls as well. Here's what we do instead:

@implementation NSView (MyViewAdditions)

static void MySetFrame(id self, SEL _cmd, NSRect frame);
static void (*SetFrameIMP)(id self, SEL _cmd, NSRect frame);

static void MySetFrame(id self, SEL _cmd, NSRect frame) {
// do custom work
SetFrameIMP(self, _cmd, frame);
}

+ (void)load {
[self swizzle:@selector(setFrame:) with:(IMP)MySetFrame store:(IMP *)&SetFrameIMP];
}

@end

While this looks a little less like Objective-C (since it's using function pointers), it avoids any naming conflicts. In principle, it's doing the exact same thing as standard swizzling. This may be a bit of a change for people who have been using swizzling as it has been defined for a while, but in the end, I think that it's better. The swizzling method is defined thusly:

typedef IMP *IMPPointer;

BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
IMP imp = NULL;
Method method = class_getInstanceMethod(class, original);
if (method) {
const char *type = method_getTypeEncoding(method);
imp = class_replaceMethod(class, original, replacement, type);
if (!imp) {
imp = method_getImplementation(method);
}
}
if (imp && store) { *store = imp; }
return (imp != NULL);
}

@implementation NSObject (FRRuntimeAdditions)
+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
return class_swizzleMethodAndStore(self, original, replacement, store);
}
@end

Swizzling by renaming methods changes the method's arguments

This is the big one in my mind. This is the reason that standard method swizzling should not be done. You are changing the arguments passed to the original method's implementation. This is where it happens:

[self my_setFrame:frame];

What this line does is:

objc_msgSend(self, @selector(my_setFrame:), frame);

Which will use the runtime to look up the implementation of my_setFrame:. Once the implementation is found, it invokes the implementation with the same arguments that were given. The implementation it finds is the original implementation of setFrame:, so it goes ahead and calls that, but the _cmd argument isn't setFrame: like it should be. It's now my_setFrame:. The original implementation is being called with an argument it never expected it would receive. This is no good.

There's a simple solution — use the alternative swizzling technique defined above. The arguments will remain unchanged!

The order of swizzles matters

The order in which methods get swizzled matters. Assuming setFrame: is only defined on NSView, imagine this order of things:

[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];

What happens when the method on NSButton is swizzled? Well most swizzling will ensure that it's not replacing the implementation of setFrame: for all views, so it will pull up the instance method. This will use the existing implementation to re-define setFrame: in the NSButton class so that exchanging implementations doesn't affect all views. The existing implementation is the one defined on NSView. The same thing will happen when swizzling on NSControl (again using the NSView implementation).

When you call setFrame: on a button, it will therefore call your swizzled method, and then jump straight to the setFrame: method originally defined on NSView. The NSControl and NSView swizzled implementations will not be called.

But what if the order were:

[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];

Since the view swizzling takes place first, the control swizzling will be able to pull up the right method. Likewise, since the control swizzling was before the button swizzling, the button will pull up the control's swizzled implementation of setFrame:. This is a bit confusing, but this is the correct order. How can we ensure this order of things?

Again, just use load to swizzle things. If you swizzle in load and you only make changes to the class being loaded, you'll be safe. The load method guarantees that the super class load method will be called before any subclasses. We'll get the exact right order!

Difficult to understand (looks recursive)

Looking at a traditionally defined swizzled method, I think it's really hard to tell what's going on. But looking at the alternative way we've done swizzling above, it's pretty easy to understand. This one's already been solved!

Difficult to debug

One of the confusions during debugging is seeing a strange backtrace where the swizzled names are mixed up and everything gets jumbled in your head. Again, the alternative implementation addresses this. You'll see clearly named functions in backtraces. Still, swizzling can be difficult to debug because it's hard to remember what impact the swizzling is having. Document your code well (even if you think you're the only one who will ever see it). Follow good practices, and you'll be alright. It's not harder to debug than multi-threaded code.

Conclusion

Method swizzling is safe if used properly. A simple safety measure you can take is to only swizzle in load. Like many things in programming, it can be dangerous, but understanding the consequences will allow you use it properly.


1 Using the above defined swizzling method, you could make things thread safe if you were to use trampolines. You would need two trampolines. At the start of the method, you would have to assign the function pointer, store, to a function that spun until the address to which store pointed to changed. This would avoid any race condition in which the swizzled method was called before you were able to set the store function pointer. You would then need to use a trampoline in the case where the implementation isn't already defined in the class and have the trampoline lookup and call the super class method properly. Defining the method so it dynamically looks up the super implementation will ensure that the order of swizzling calls does not matter.

How can I remove an element of main.storyboard

These are called Navigation buttons and, If you hide them, Apple will reject your WebView app with the 4.2 minimum functionality, But either way, to do so- Paste this below viewDidLoad():

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavigationBarHidden(true, animated: animated) // Hide
}

More info How to hide a navigation bar from first ViewController in Swift?

How can I mimic the bottom sheet from the Maps app?

I don't know how exactly the bottom sheet of the new Maps app, responds to user interactions. But you can create a custom view that looks like the one in the screenshots and add it to the main view.

I assume you know how to:

1- create view controllers either by storyboards or using xib files.

2- use googleMaps or Apple's MapKit.

Example

1- Create 2 view controllers e.g, MapViewController and BottomSheetViewController. The first controller will host the map and the second is the bottom sheet itself.

Configure MapViewController

Create a method to add the bottom sheet view.

func addBottomSheetView() {
// 1- Init bottomSheetVC
let bottomSheetVC = BottomSheetViewController()

// 2- Add bottomSheetVC as a child view
self.addChildViewController(bottomSheetVC)
self.view.addSubview(bottomSheetVC.view)
bottomSheetVC.didMoveToParentViewController(self)

// 3- Adjust bottomSheet frame and initial position.
let height = view.frame.height
let width = view.frame.width
bottomSheetVC.view.frame = CGRectMake(0, self.view.frame.maxY, width, height)
}

And call it in viewDidAppear method:

override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
addBottomSheetView()
}

Configure BottomSheetViewController

1) Prepare background

Create a method to add blur and vibrancy effects

func prepareBackgroundView(){
let blurEffect = UIBlurEffect.init(style: .Dark)
let visualEffect = UIVisualEffectView.init(effect: blurEffect)
let bluredView = UIVisualEffectView.init(effect: blurEffect)
bluredView.contentView.addSubview(visualEffect)

visualEffect.frame = UIScreen.mainScreen().bounds
bluredView.frame = UIScreen.mainScreen().bounds

view.insertSubview(bluredView, atIndex: 0)
}

call this method in your viewWillAppear

override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
prepareBackgroundView()
}

Make sure that your controller's view background color is clearColor.

2) Animate bottomSheet appearance

override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)

UIView.animateWithDuration(0.3) { [weak self] in
let frame = self?.view.frame
let yComponent = UIScreen.mainScreen().bounds.height - 200
self?.view.frame = CGRectMake(0, yComponent, frame!.width, frame!.height)
}
}

3) Modify your xib as you want.

4) Add Pan Gesture Recognizer to your view.

In your viewDidLoad method add UIPanGestureRecognizer.

override func viewDidLoad() {
super.viewDidLoad()

let gesture = UIPanGestureRecognizer.init(target: self, action: #selector(BottomSheetViewController.panGesture))
view.addGestureRecognizer(gesture)

}

And implement your gesture behaviour:

func panGesture(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translationInView(self.view)
let y = self.view.frame.minY
self.view.frame = CGRectMake(0, y + translation.y, view.frame.width, view.frame.height)
recognizer.setTranslation(CGPointZero, inView: self.view)
}

Scrollable Bottom Sheet:

If your custom view is a scroll view or any other view that inherits from, so you have two options:

First:

Design the view with a header view and add the panGesture to the header. (bad user experience).

Second:

1 - Add the panGesture to the bottom sheet view.

2 - Implement the UIGestureRecognizerDelegate and set the panGesture delegate to the controller.

3- Implement shouldRecognizeSimultaneouslyWith delegate function and disable the scrollView isScrollEnabled property in two case:

  • The view is partially visible.
  • The view is totally visible, the scrollView contentOffset property is 0 and the user is dragging the view downwards.

Otherwise enable scrolling.

  func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
let gesture = (gestureRecognizer as! UIPanGestureRecognizer)
let direction = gesture.velocity(in: view).y

let y = view.frame.minY
if (y == fullView && tableView.contentOffset.y == 0 && direction > 0) || (y == partialView) {
tableView.isScrollEnabled = false
} else {
tableView.isScrollEnabled = true
}

return false
}

NOTE

In case you set .allowUserInteraction as an animation option, like in the sample project, so you need to enable scrolling on the animation completion closure if the user is scrolling up.

Sample Project

I created a sample project with more options on this repo which may give you better insights about how to customise the flow.

In the demo, addBottomSheetView() function controls which view should be used as a bottom sheet.

Sample Project Screenshots

- Partial View

Sample Image

- FullView

Sample Image

- Scrollable View

Sample Image



Related Topics



Leave a reply



Submit