In normal development, a loading indicator (or a progress bar) is displayed on the screen while working on some time-consuming task. This lets the user know that the app is doing something and not just sitting there stuck.

MBProgressHUD: As a developer of iOS development, I’ve probably heard of it, if not used, and as a third party loading animations, my own feeling about this library is that it’s simple and easy to use. MBProgressHUD is a very simple implementation, with a custom view and a display and collapse method. I just wrapped it up and started using it. Now looking back, in addition to see how the author realized, I have other harvest, especially the author’s code layout, clear and easy to understand, after seeing the author’s code, the author’s own intuitive feeling is very carefree, willing to see.

Classes involved

First let’s look at the classes involved in this library:

MBProgressHUD: Core class, which we call externally directly, generates an instance of the class, and then adds it to the view or window we want.

MBBackgroundView: As can be seen from the class name, this class acts as a background view. The author customizes this class by adding a UIVisualEffectView to the view to display the blurred effect.

MBRoundProgressView: custom circular loading view.

MBBarProgressView: Custom bar load view.

MBProgressHUDRoundedButton: this is a private class, there is no public inheritance in a class of UIButton. The intrinsicContentSize method is overwritten to increase the intrinsic size of the button to its width; Rounded corners and borders were added.


Let’s focus on the MBProgressHUD implementation:

Initialization:

#pragma mark
- (instancetype)initWithFrame:(CGRect)frame
- (instancetype)initWithView:(UIView *)view
commonInit

The registerForNotifications method is called in the commonInit method to add the observer to the notification center, while being removed directly from the dealloc method. Notifications are clearly added and removed in pairs to avoid forgetting to remove observers.

The UI layout

setupViews
updateIndicators
mode

Let’s take a look at the view hierarchy that the HUD finally presents:

- (void)setupViews {UIColor *defaultColor = self.contentColor; MBBackgroundView *backgroundView = [[MBBackgroundView alloc] initWithFrame:self.bounds]; ,,,,,,,, [self addSubview: backgroundView]; _backgroundView = backgroundView; MBBackgroundView *bezelView = [MBBackgroundView new]; 、、、、、、 [self addSubview:bezelView]; _bezelView = bezelView; [self updateBezelMotionEffects]; UILabel *label = [UILabel new]; 、、、、、、 _label = label; UILabel *detailsLabel = [UILabel new]; 、、、、、、 _detailsLabel = detailsLabel; UIButton *button = [MBProgressHUDRoundedButton buttonWithType:UIButtonTypeCustom]; 、、、、、、 _button = button;for (UIView *view in @[label, detailsLabel, button]) {
        view.translatesAutoresizingMaskIntoConstraints = NO;
        [view setContentCompressionResistancePriority:998.f forAxis:UILayoutConstraintAxisHorizontal];
        [view setContentCompressionResistancePriority:998.f forAxis:UILayoutConstraintAxisVertical]; [bezelView addSubview:view]; } // These two views are hidden by default and used for constraints. UIView *topSpacer = [UIView new]; topSpacer.translatesAutoresizingMaskIntoConstraints = NO; topSpacer.hidden = YES; [bezelView addSubview:topSpacer]; _topSpacer = topSpacer; UIView *bottomSpacer = [UIView new]; bottomSpacer.translatesAutoresizingMaskIntoConstraints = NO; bottomSpacer.hidden = YES; [bezelView addSubview:bottomSpacer]; _bottomSpacer = bottomSpacer; }Copy the code

The MBProgressHUD UI layout is illustrated by the hierarchy diagram and code: to make it easier to see, I have set different colors for different views.

  • Red: The lowest red view in the hierarchy isMBProgressHUDInstance HUD of the.
  • Yellow: The yellow view in the hierarchy isMBBackgroundViewAs the background view for the HUDbackgroundView.
  • So is the green viewMBBackgroundViewInstances of the classbezelView, as the container view that actually displays the loading diagram. Layout on this viewlabel,detailLabel,indicator, andbutton. Among themindicatorAs a private property, according to the hud SettingsmodeDifferent, thus setting differentindicator, so when the author sets the propertyindicatorSet the property type to UIView.

Here to remind is: if you want to set the mode to MBProgressHUDModeCustomView, is you don’t want to use a third party to provide some of the indicator view, to their own, you must realize intrinsicContentSize custom view method, Get a good size. Since the author’s layout is constrained and does not utilize frames, the author notes in the method comments that the custom view needs to implement the intrinsicContentSize intrinsicContentSize method to get a proper size. UILabel, UIButton, and UIImageView all implement intrinsicContentSize by default. If your custom view inherits directly from the UIView class, intrinsicContentSize, intrinsicContentSize, intrinsicContentSize; Then you need to implement the intrinsicContentSize method.

Show&hide function implementation

@property (nonatomic, weak) NSTimer *graceTimer;
@property (nonatomic, weak) NSTimer *minShowTimer;
@property (nonatomic, weak) NSTimer *hideDelayTimer;
@property (nonatomic, weak) CADisplayLink *progressObjectDisplayLink;
Copy the code

We will see the author’s implementation from the use of these four timers:

  • graceTimer: An attribute associated with graceTimer isgraceTime: Grace period. The purpose of this property is to eliminate the need to pop up the HUD when the task is executing quickly. This is equivalent to setting a minimum time for your task, such as 0.5; When your task takes more than 0.5 seconds, the HUD will trigger the pop-up display.
- (void)showAnimated:(BOOL)animated {
    MBMainThreadAssert();
    [self.minShowTimer invalidate];
    self.useAnimation = animated;
    self.finished = NO;
    // If the grace time is set// After graceTime is set, the pop-up display of the HUD will be delayed and triggered by self.graceTimer.if(self. GraceTime > 0.0) {NSTimer * timer = [NSTimer timerWithTimeInterval: self. GraceTime target: the self selector:@selector(handleGraceTimer:) userInfo:nil repeats:NO]; [[NSRunLoop currentRunLoop] addTimer:timerforMode:NSRunLoopCommonModes];
        self.graceTimer = timer;
    } 
    // ... otherwise show the HUD immediately
    else{ [self showUsingAnimation:self.useAnimation]; }}Copy the code
  • minShowTimer: Displays the minimum time timer. The associated properties areminShowTime. Avoid hiding the HUD as soon as it is displayed.
- (void)hideAnimated:(BOOL)animated { MBMainThreadAssert(); //hud's show and hide operations must be performed in the main thread. [self.graceTimer invalidate]; GraceTimer: self.graceTimer: self.graceTimer: self.graceTimer: self.graceTimer: self.graceTimer: self.graceTimer self.useAnimation = animated; self.finished = YES; // If the minShow time isset, calculate how long the HUD was shown,
    // and postpone the hiding operation if necessary
    if(self.minShowTime > 0.0 && self.showStarted) {// Avoid instant display and collapse, if minimum display time is set here, calculate how long it will take to start self.minShowTimer to collapse the HUD. NSTimeInterval interv = [[NSDate date] timeIntervalSinceDate:self.showStarted];if (interv < self.minShowTime) {
            NSTimer *timer = [NSTimer timerWithTimeInterval:(self.minShowTime - interv) target:self selector:@selector(handleMinShowTimer:) userInfo:nil repeats:NO];
            [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
            self.minShowTimer = timer;
            return; }} / /... otherwise hide the HUD immediately [self hideUsingAnimation:self.useAnimation]; }Copy the code
  • hideDelayTimer: delay hide timer, this timer is mainly for- (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delayInterface.
- (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay {
    // Cancel any scheduled hideAnimated:afterDelay: calls
    [self.hideDelayTimer invalidate];
    NSTimer *timer = [NSTimer timerWithTimeInterval:delay target:self selector:@selector(handleHideTimer:) userInfo:@(animated) repeats:NO];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    self.hideDelayTimer = timer;
}
Copy the code

If this method is repeatedly called, the previous hideDelayTimer will be abandoned, and a new timer will start to hide the timing delay.

  • progressObjectDisplayLink: This timer is triggered according to the frame rate of the screen refreshupdateProgressFromProgressObjectMethod for refreshing the progress bar. And this timer is only in the settingprogressObjectProperty is created after. Through thisprogressObjectProperty feeds progress information back to the HUD to continuously update the progress bar.

To sum up: Through the operation of the above four timers, we can see the logical processing of HUD eject and retractable. Finally, the HUD is displayed according to the deformation of the bezelView. The huD-hidden callback can be done by completionBlock or by setting the agent for MBProgressHUDDelegate.

- (void)done {
    [self setNSProgressDisplayLinkEnabled:NO]; / / hide, will be progressObjectDisplayLink deprecated failureif(self.hasFinished) {self.alpha = 0.0f;if(self.removeFromSuperViewOnHide) { [self removeFromSuperview]; // } } MBProgressHUDCompletionBlock completionBlock = self.completionBlock; // Trigger the callback blockif(completionBlock) { completionBlock(); <MBProgressHUDDelegate> delegate = self.delegate;if([delegate respondsToSelector:@selector(hudWasHidden:)]) { [delegate performSelector:@selector(hudWasHidden:) withObject:self]; }}Copy the code

Unit testing

From the above HUD implementation, we can see that the author has carefully separated initialization, layout, constraints, pop-up, hide and related attribute Settings as separate methods. This makes it easy to unit test each method as a use case.

Check it out with #pragma Mark:

- (void)testInitializers {
    XCTAssertNotNil([[MBProgressHUD alloc] initWithView:[UIView new]]);
    UIView *nilView = nil;
    XCTAssertThrows([[MBProgressHUD alloc] initWithView:nilView]);
    XCTAssertNotNil([[MBProgressHUD alloc] initWithFrame:CGRectZero]);
    NSKeyedUnarchiver *dummyUnarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:[NSData data]];
    XCTAssertNotNil([[MBProgressHUD alloc] initWithCoder:dummyUnarchiver]);
}
Copy the code

Let’s look at test initializers. The author tests all the specified initializers he gives, both normal and abnormal.

Finally, after reading the MBProgressHUD source code, I believe that the biggest gains are the cleanliness and organization of the author’s code, as well as the logical use case division, which makes it easy to unit test and ensure the quality of the code.

The above is my own study notes, if there is any misunderstanding, please point out, thank you!