FIFO and LIFO auto-manage modal controller

Posted by thom2002 on Tue, 09 Jul 2019 20:07:13 +0200

In an App, pop-ups have always been a frequently used prompt control.It seems simple, but it has a lot of deep things to learn from.

As we all know, how much importance Apple attaches to the user experience is reflected in how it handles pop-ups.I don't know if you've noticed the order in which pop-ups appear on newly installed Apps?Usually this is the case, if a pop-up window for notification of permissions first appears, then a pop-up window for positioning permissions, the previous pop-up window will be temporarily hidden, and then pop-up again after the user closes the pop-up window.Apparently Apple thinks the pop-up window behind is more important, so it should be handled by users first.Apple manages the pop-up order of pop-ups in LIFO (last in, first out).

Later in this article, we will explain how to use Runtime and multithreading to implement LIFO and FIFO (first in, last out).Take a look at the final result, a UIViewController for a custom Transition Animation.


Final results

UIAlertView supports automatic LIFO management.However, it has been obsolete since iOS8, and when UIAlertView is used again, yellow warnings appear in the code.Here's a description of the abandoned UIAlertView, which Apple lets us replace with UIAlertController.

UIAlert View is deprecated in iOS 8. (Note that UIAlert View Delegate is also deprecated.) To create and manage alerts in iOS 8 and later, instead use UIAlert Controller with a preferred Style of UIAlert Controller Style Alert.

UIAlertController is a controller that inherits from UIViewController and customizes the transit animation. Like other modal controllers, it calls presentViewController:animated:completion:method pop-up.However, each controller can only have one presentedController, that is, only one other controller can be presented at a time. The following prompt will appear when pressing forcibly:

Warning: Attempt to present <UIAlertController: 0x7fdb635045d0>  on <ViewController: 0x7fdb660090f0> which is already presenting <UIAlertController: 0x7fdb635084a0>

In many cases, after an asynchronous request ends, we need to prompt for a pop-up window based on the return information from the server.However, it is not guaranteed that the controller at that time already present entedanother UIAlertController, and the new pop-up window is not pop-up.So using UIAlertController can be a big problem for us.

Another way to do pop-ups is to cover a custom UIView with a UIWindow.First, this method does not support pop-up sequence management. Second, multiple pop-ups at the same time is to execute the addSubview method multiple times. Many translucent background masks and views are overlapped together, so the display effect is self-evident.More importantly, many system controls also use windows as a parent control, such as the keyboard window which is UITextEffectsWindow. Frequent use of windows can cause many unexpected problems.

Summarize the three pop-up window modes mentioned above:

Bounce window mode Problems
UIAlertView iOS8 has been abandoned since
UIAlertController You can only play one at a time
-[UIWindow addSubview:] Showing multiple popups at the same time is less effective

Now start explaining how to manage modal controllers with FIFO and LIFO

Start with a relatively simple FIFO


FIFO Flowchart

The effect is as follows

ps: gif chart, pop-up window 4 in pop-up window 2 Click event, observe the difference between FIFO and LIFO


Customize UIViewController

UIAlertController

First, a new UIViewController classification is created, which is designed to be 100% decoupled for controller invocation and to support modal management of FIFO and LIFO for all controllers that inherit from UIViewController.

Categorized headers have only one method to replace the system's presentViewController:animated:completion:method.This method receives a UIViewController parameter, a present-completed callback, and a dismiss-completed callback.

// Enumeration, how do you want to manage the modal controller
typedef NS_OPTIONS (NSUInteger, JCPresentType) {
    JCPresentTypeLIFO = 0, // last in, first out
    JCPresentTypeFIFO      // first in, last out
};
// New present Method
- (void)jc_presentViewController:(UIViewController *)controller presentType:(JCPresentType)presentType presentCompletion:(void (^)(void))presentCompletion dismissCompletion:(void (^)(void))dismissCompletion;

In the.m file, the method is implemented as follows

// Judge JCPresentType enumeration type, jump to specific method
- (void)jc_presentViewController:(UIViewController *)controller presentType:(JCPresentType)presentType presentCompletion:(void (^)(void))presentCompletion dismissCompletion:(void (^)(void))dismissCompletion {
    if (presentType == JCPresentTypeLIFO) {
        [self lifoPresentViewController:controller presentCompletion:presentCompletion dismissCompletion:dismissCompletion];
    } else {
        [self fifoPresentViewController:controller presentCompletion:presentCompletion dismissCompletion:dismissCompletion];
    }
}
// Core approach
- (void)fifoPresentViewController:(UIViewController *)controller presentCompletion:(void (^)(void))presentCompletion dismissCompletion:(void (^)(void))dismissCompletion {
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        [controller setDeallocCompletion:^{
            if (dismissCompletion) {
                dismissCompletion();
            }
            // got to next operation
            dispatch_semaphore_signal(semaphore);
        }];
        dispatch_async(dispatch_get_main_queue(), ^{
            [self presentViewController:controller animated:YES completion:presentCompletion];
        });
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    }];

    // put in queue
    if ([self getOperationQueue].operations.lastObject) {
        [operation addDependency:[self getOperationQueue].operations.lastObject];
    }
    [[self getOperationQueue] addOperation:operation];
}
// NSOperationQueue singleton for adding operation s
- (NSOperationQueue *)getOperationQueue {
    static NSOperationQueue *operationQueue = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        operationQueue = [NSOperationQueue new];
    });
    return operationQueue;
}
// Access deallocCompletion block using associated objects
- (void)setDeallocCompletion:(void (^)(void))completion {
    objc_setAssociatedObject(self, @selector(getDeallocCompletion), completion, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (void (^)(void))getDeallocCompletion {
    return objc_getAssociatedObject(self, _cmd);
}
// The hook controller's viewDidDisappear method, which calls the deallocCompletion block at this time
+ (void)load {
    SEL oldSel = @selector(viewDidDisappear:);
    SEL newSel = @selector(jc_viewDidDisappear:);
    Method oldMethod = class_getInstanceMethod([self class], oldSel);
    Method newMethod = class_getInstanceMethod([self class], newSel);

    BOOL didAddMethod = class_addMethod(self, oldSel, method_getImplementation(newMethod), method_getTypeEncoding(newMethod));
    if (didAddMethod) {
        class_replaceMethod(self, newSel, method_getImplementation(oldMethod), method_getTypeEncoding(oldMethod));
    } else {
        method_exchangeImplementations(oldMethod, newMethod);
    }
}

- (void)jc_viewDidDisappear:(BOOL)animated {
    [self jc_viewDidDisappear:animated];

    if ([self getDeallocCompletion] && ![self isTemporarilyDismissed]) {
        [self getDeallocCompletion]();
    }
}

Where dispatch_semaphore_t is a semaphore in a multithread, the dispatch_semaphore_signal function adds one semaphore, dispatch_semaphore_wait subtracts one semaphore, and pauses the current thread when the semaphore is less than 0.

presentViewController is a time-consuming operation that I place in NSBlockOperation and pause the thread with dispatch_semaphore_t until present ation completes.Each NSBlockOperation's forward and backward dependencies are set and added to the NSOperationQueue to form a serial FIFO queue.

Let's start with LIFO


LIFO Flowchart

The effect is as follows


Customize UIViewController

UIAlertController
// Core approach
- (void)lifoPresentViewController:(UIViewController *)controller presentCompletion:(void (^)(void))presentCompletion dismissCompletion:(void (^)(void))dismissCompletion {

    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    // put in stack
    NSMutableArray *stackControllers = [self getStackControllers];
    if (![stackControllers containsObject:controller]) {
        [stackControllers addObject:controller];
    }

    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        __weak typeof(controller) weakController = controller;
        [controller setPresentCompletion:presentCompletion];
        [controller setDismissCompletion:dismissCompletion];
        [controller setDeallocCompletion:^{
            if (dismissCompletion) {
                dismissCompletion();
            }

            // fetch new next controller if exists, because button action after dismiss completion
            [weakController setDismissing:YES];
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(CGFLOAT_MIN * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                [weakController setDismissing:NO];
                // if the dismiss controller is the last one
                if (stackControllers.lastObject == controller) {
                    [stackControllers removeObject:weakController];

                    // is there any previous controllers
                    if (stackControllers.count > 0) {
                        UIViewController *preController = [stackControllers lastObject];
                        [self lifoPresentViewController:preController presentCompletion:[preController getPresentCompletion] dismissCompletion:[preController getDismissCompletion]];
                    }
                } else {
                    NSUInteger index = [stackControllers indexOfObject:weakController];
                    [stackControllers removeObject:weakController];

                    // is there any next controllers
                    NSArray *nextControllers = [stackControllers objectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(index, stackControllers.count - index)]];
                    for (UIViewController *nextController in nextControllers) {
                        [self lifoPresentViewController:nextController presentCompletion:[nextController getPresentCompletion] dismissCompletion:[nextController getDismissCompletion]];
                    }
                }
            });
        }];

        // if the previous controller is dismissing, wait it's completion
        if (stackControllers.count > 1) {
            for (UIViewController *preController in stackControllers) {
                if ([preController isDismissing]) {
                    return ;
                }
            }
        }

        // present a new controller before dismissing the presented controller if exists
        dispatch_async(dispatch_get_main_queue(), ^{
            if (self.presentedViewController) {
                [self.presentedViewController temporarilyDismissViewControllerAnimated:YES completion:^{
                    [self presentViewController:controller animated:YES completion:^{
                        dispatch_semaphore_signal(semaphore);
                    }];
                }];
            } else {
                [self presentViewController:controller animated:YES completion:^{
                    dispatch_semaphore_signal(semaphore);
                    if (presentCompletion) {
                        presentCompletion();
                    }
                }];
            }
        });

        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    }];

    // put in queue
    if ([self getOperationQueue].operations.lastObject) {
        [operation addDependency:[self getOperationQueue].operations.lastObject];
    }
    [[self getOperationQueue] addOperation:operation];
}
// Accessing dismissCompletion using associated objects
- (void)setDismissCompletion:(void (^)(void))completion {
    objc_setAssociatedObject(self, @selector(getDismissCompletion), completion, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (void (^)(void))getDismissCompletion {
    return objc_getAssociatedObject(self, _cmd);
}
// Accessing presentCompletion using associated objects
- (void)setPresentCompletion:(void (^)(void))completion {
    objc_setAssociatedObject(self, @selector(getPresentCompletion), completion, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (void (^)(void))getPresentCompletion {
    return objc_getAssociatedObject(self, _cmd);
}

// Access temporarilyDismissed using associated objects to determine whether the controller is temporarily hidden or closed by the user
- (void)setTemporarilyDismissed:(BOOL)temporarilyDismissed {
    objc_setAssociatedObject(self, @selector(isTemporarilyDismissed), @(temporarilyDismissed), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (BOOL)isTemporarilyDismissed {
    NSNumber *num = objc_getAssociatedObject(self, _cmd);
    return [num boolValue];
}

// Accessing dismissing using associated objects
- (void)setDismissing:(BOOL)dismissing {
    objc_setAssociatedObject(self, @selector(isDismissing), @(dismissing), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (BOOL)isDismissing {
    NSNumber *num = objc_getAssociatedObject(self, _cmd);
    return [num boolValue];
}

// Array stack, used to cache all incoming controllers
- (NSMutableArray *)getStackControllers {
    static NSMutableArray *stackControllers = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        stackControllers = [NSMutableArray array];
    });
    return stackControllers;
}

// Temporary dismiss method
- (void)temporarilyDismissViewControllerAnimated: (BOOL)flag completion: (void (^ __nullable)(void))completion {
    [self setTemporarilyDismissed:YES];
    [self dismissViewControllerAnimated:flag completion:^{
        [self setTemporarilyDismissed:NO];
        if (completion) {
            completion();
        }
    }];
}

The difference between the LIFO method and FIFO method is that each time you come in, you will first determine if there is already a controller in front of you and temporarily dismiss if there is one.Moreover, when the controller is shut down by the user, it is preferred to determine whether there are controllers that have not yet popped up behind the stack before deciding whether there are controllers in front of the stack.

In this way, a present classification method with FIFO and LIFO drivers is finished. Try it out.

Download address: UIViewController+JCPresentQueue.h

Reference material

Topics: iOS Windows less