1. Introduction

MJRefresh is the daily iOS development in the use of a relatively high frequency of a drop-down refresh/pull up to load more third-party controls, usually does not seem to have a complete view of the source code, here is a record of exploring the source code process.

Note: This article has been synchronized to personal blog.

2. Example

There are many types of refresh styles provided in the official Example. This article only discusses two of them (UITableView + pull-down refresh animation picture and UITableView + pull-up refresh animation picture) as examples.

Example 1: UITableView + drop-down refresh animated picture

- (void)exampleA
{
    // 1. Set the header
    self.tableView.mj_header = [MJChiBaoZiHeader headerWithRefreshingTarget:self refreshingAction:@selector(loadNewData)];
    // 2. Enter the refresh state immediately
    [self.tableView.mj_header beginRefreshing];
}

- (void)loadNewData {
    // 3. Download data
   __weak typeof(self) weakSelf = self;
   [self requestNetDataWithCompletionBlock:^(NSArray *result, BOOL isSuccess) {
   
      // 4. Processing returned data (omitted)

      // 5. Refresh the table and end the refresh state
      [weakSelf.tableView reloadData];
      // 6. Get the current drop-down refresh control,
      [weakSelf.tableView.mj_header endRefreshing];
   }];
}
Copy the code

This is a common usage scenario, where steps 1, 2, 5, and 6 are related actions of MJRefresh.

Example 2: UITableView + pull up refresh animated picture

- (void)exampleB
{
    1. Set footer
    self.tableView.mj_footer = [MJChiBaoZiFooter footerWithRefreshingTarget:self refreshingAction:@selector(loadMoreData)];
}

- (void)loadNewData {
    // 2. Download data
   __weak typeof(self) weakSelf = self;
   [self requestNetDataWithCompletionBlock:^(NSArray *result, BOOL isSuccess) {
      // 3. Append returned data to table data source (omitted)

      // 4. Refresh the table and end the refresh state
      [weakSelf.tableView reloadData];
      // 5. Get the current drop-down to load more controls,[weakSelf.tableView.mj_footer endRefreshing]; }}Copy the code

Similar to the drop-down refresh, steps 1, 4, and 5 here are related actions of MJRefresh.

3. The big picture

Before starting to analyze the source code, I thought I’d take a look at the basic implementation ideas of the library so that I wouldn’t get confused when looking at the source code. Below pull refresh as an example, do a simple introduction, first look at the following figure.

First of all, when we assign to tableView.mj_header, we actually add a child view, a refresh control, to the tableView, but it’s not added to the tableView’s header, so it doesn’t occupy the tableView’s header.

Then, the tableView listens (KVO), and the refresh control intercepts when the contentOffset of TableViews changes, Update the display of the control and the contentinset.top of the tableView according to the Y value of contentOffset, as shown in the figure above. Let’s talk about this picture:

① Just add the refresh control to the tableView, set the refresh control y value for the negative value of its own height, at this time to change the control will be blocked by the navigation, of course, you can also set its transparency to 0.

② Drop down tableView, when the refresh control is fully displayed (critical point) before, is a state, let go at this time, will directly bounce back.

(3) After the critical point, and then pull down, update the display of the control, then let go of the start to refresh.

④ Let go of the refresh, the control will bounce back, you can add animation, not so stiff. Simultaneously executes a block passed in by the caller, typically requesting network data.

(5) to refresh the process, to show the refresh controls, that is, don’t let it bounce to navigation, behind will give tableView. ContentInset. The height of the top add a control, of course is negative.

After 6 when the caller requests the data, manual call refresh control endRefreshing method, in this method inside the class UI update control to the initial state, and the tableView. ContentInset. Top to reduce the height of a control, of course also is negative, so far, refresh the end.

The above basic implementation of the logic, the following start to see the source code.

4. Source code analysis

Let’s explore drop-down refresh and drop-down load for more source code implementations, respectively.

As can be inferred from Examples 1 and 2, the framework can be roughly divided into two parts. One part is the carrier of the refresh control (UIScrollView and its subclasses, namely tableView and collectionView), and the other part is the refresh control itself. These are called headers and footers.

4.1 Refresh the control carrier

The carrier is mainly concentrated in these categories below

UIScrollView+MJExtension
UIScrollView+MJRefresh
UIView+MJExtension
Copy the code

UIView+MJExtension

UIView+MJExtension simply provides convenient access to the frame of the common base UIView class, including the refresh control.

UIScrollView+MJRefresh

UIScrollView+MJRefresh is the classification of the base class of the list. This file actually contains three classifications, one in order

NSObject (MJRefresh) : NSObject (MJRefresh) : NSObject

/// exchange instance methods
+ (void)exchangeInstanceMethod1:(SEL)method1 method2:(SEL)method2
{
    method_exchangeImplementations(class_getInstanceMethod(self, method1), class_getInstanceMethod(self, method2));
}
/// switch class methods
+ (void)exchangeClassMethod1:(SEL)method1 method2:(SEL)method2
{
    method_exchangeImplementations(class_getClassMethod(self, method1), class_getClassMethod(self, method2));
}
Copy the code

(2) the UIScrollView (MJRefresh) : Header, footer, and mj_reloadDataBlock are added to the base UIScrollView, and the corresponding setter and getter implementations are added using the associated object. See Effective Objective-C 2.0, No. 10, for an introduction to using associated objects to store custom data in existing classes.

Two methods, willChangeValueForKey: and didChangeValueForKey:, were added before and after setting the association object in the setter of mj_reloadDataBlock to add KVO listeners.

- (void)setMj_reloadDataBlock:(void(^) (NSInteger))mj_reloadDataBlock
{
    [self willChangeValueForKey:@"mj_reloadDataBlock"]; // KVO
    objc_setAssociatedObject(self, &MJRefreshReloadDataBlockKey, mj_reloadDataBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
    [self didChangeValueForKey:@"mj_reloadDataBlock"]; // KVO
}
Copy the code

It then provides a method to execute mj_reloadDataBlock, executeReloadDataBlock:

- (void)executeReloadDataBlock
{
    !self.mj_reloadDataBlock ? : self.mj_reloadDataBlock(self.mj_totalDataCount);
}
Copy the code

That is, if mj_reloadDataBlock is set, the block is executed there. We notice that mj_totalDataCount refers to the total number of rows in a UITableView or UICollectionView.

- (NSInteger)mj_totalDataCount
{
    NSInteger totalCount = 0;
    if ([self isKindOfClass:[UITableView class]]) {
        UITableView *tableView = (UITableView *)self;
        
        for (NSInteger section = 0; section<tableView.numberOfSections; section++) { totalCount += [tableView numberOfRowsInSection:section]; }}else if ([self isKindOfClass:[UICollectionView class]]) {
        UICollectionView *collectionView = (UICollectionView *)self;
        
        for (NSInteger section = 0; section<collectionView.numberOfSections; section++) { totalCount += [collectionView numberOfItemsInSection:section]; }}return totalCount;
}
Copy the code

③④ UITableView (MJRefresh) and UICollectionView (MJRefresh) provide the following two methods:

+ (void)load
{
    [self exchangeInstanceMethod1:@selector(reloadData) method2:@selector(mj_reloadData)];
}

- (void)mj_reloadData
{
    [self mj_reloadData];
    [self executeReloadDataBlock];
}
Copy the code

That is, the list reloadData method is swapped with the custom mj_reloadData method when the load method is executed at program startup, adding a step to the new method [self executeReloadDataBlock]; Mj_reloadDataBlock, so that when we execute the tableView reloadData method, we actually execute the mj_reloadData method.

When is this block set? A global search shows that it is only set in the willMoveToSuperview: method of MJRefreshFooter, which is when the footer is added to the tableView.

- (void)willMoveToSuperview:(UIView *)newSuperview
{
    [super willMoveToSuperview:newSuperview];
    
    // When the current view is added to the parent view, set the following block, that is, hide the current view when the list (UICollectionView or UITableView) line number is 0.
    if (newSuperview) {
        // Listen for scrollView data changes
        if ([self.scrollView isKindOfClass:[UITableView class]] | | [self.scrollView isKindOfClass:[UICollectionView class{[]])self.scrollView setMj_reloadDataBlock:^(NSInteger totalDataCount) {
                if (self.isAutomaticallyHidden) {
                    self.hidden = (totalDataCount == 0); }}]; }}}Copy the code

WillMoveToSuperview: called when a view is added to or removed from the superview, if (newSuperview) {… } this block is set when the footer is added to the superview. If automatic hiding is required, hide the footer when the total number of entries is 0, otherwise display the footer.

UIScrollView+MJExtension

UIScrollView+MJExtension provides easy access to the following attributes:

contentInset / adjustedContentInset
contentOffset
contentSize
Copy the code

AdjustedContentInset is a new property introduced in iOS 11. In iOS 11, the adjustedContentInset property, not contentInset, determines how far the content of a tableView is from the edge.

4.2 Drop down Refresh control (refreshHeader)

Let’s start with a picture:

The header inheritance relationship is shown in the figure above. In the example, MJChiBaoZiHeader is inherited from MJRefreshGifHeader. To make the description more logical, let’s start with the base class.

MJRefreshComponent

The MJRefreshComponent is the base class for all headers and footers and defines an enumeration of refresh state MJRefreshState and three different callbacks.

/** Refresh the state of the control */
typedef NS_ENUM(NSInteger, MJRefreshState) {
    /** Normal idle state */
    MJRefreshStateIdle = 1./** State that can be refreshed when released */
    MJRefreshStatePulling,
    /** The state being refreshed */
    MJRefreshStateRefreshing,
    /** The state to be refreshed */
    MJRefreshStateWillRefresh,
    /** All data loaded, no more data */
    MJRefreshStateNoMoreData
};

/** Enter the refresh state callback */
typedef void (^MJRefreshComponentRefreshingBlock)(void);
/** Callback after the start of the refresh (callback after entering the refresh state) */
typedef void (^MJRefreshComponentbeginRefreshingCompletionBlock)(void);
/** End the refresh callback */
typedef void (^MJRefreshComponentEndRefreshingCompletionBlock)(void);
Copy the code

There are actually two classes in the class file, with a UILabel classification in addition to itself, providing a class method mj_label to create a custom Label and an instance method mj_textWith to get the text width.

+ (instancetype)mj_label
{
    UILabel *label = [[self alloc] init];
    label.font = MJRefreshLabelFont;
    label.textColor = MJRefreshLabelTextColor;
    label.autoresizingMask = UIViewAutoresizingFlexibleWidth;
    label.textAlignment = NSTextAlignmentCenter;
    label.backgroundColor = [UIColor clearColor];
    return label;
}

- (CGFloat)mj_textWith {
    CGFloat stringWidth = 0;
    CGSize size = CGSizeMake(MAXFLOAT, MAXFLOAT);
    if (self.text.length > 0) {
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000
        stringWidth =[self.text
                      boundingRectWithSize:size
                      options:NSStringDrawingUsesLineFragmentOrigin
                      attributes:@{NSFontAttributeName:self.font}
                      context:nil].size.width;
#else
        
        stringWidth = [self.text sizeWithFont:self.font
                            constrainedToSize:size
                                lineBreakMode:NSLineBreakByCharWrapping].width;
#endif
    }
    return stringWidth;
}
Copy the code

Let’s take a look at the MJRefreshComponent class, which, as usual, starts with the initialization method:

- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame]) {
        // Preparations
        [self prepare];
        
        // The default state is normal
        self.state = MJRefreshStateIdle;
    }
    return self;
}

- (void)prepare
{
    // Basic attributes
    self.autoresizingMask = UIViewAutoresizingFlexibleWidth;
    self.backgroundColor = [UIColor clearColor];
}
Copy the code

Several variables are initialized:

(1) The initial state is set to the normal state, and no refresh is triggered;

(2) prepare method set up two basic attributes, backgroundColor and autoresizingMask autoresizingMask initial UIViewAutoresizingFlexibleWidth refers to: When the bounds of the parent view change, the child view (the current view) automatically adjusts the width to keep the left and right margins the same.

For layout, the layoutSubviews method is overridden, adding a placeSubviews step before calling the super method, which requires subclasses to implement.

- (void)layoutSubviews
{
    [self placeSubviews];
    
    [super layoutSubviews];
}

- (void)placeSubviews {
    
}
Copy the code

Overridden willMoveToSuperview: this method is used to add additional actions when adding the current view to or removing it from the superview.

- (void)willMoveToSuperview:(UIView *)newSuperview
{
    [super willMoveToSuperview:newSuperview];
    
    // If it is not UIScrollView, do nothing
    if(newSuperview && ! [newSuperview isKindOfClass:[UIScrollView class]]) return;
    
    // Remove the listener from the old parent control
    [self removeObservers];
    
    if (newSuperview) { // The new parent control
        // Set the width
        self.mj_w = newSuperview.mj_w;
        // Set the location
        self.mj_x = -_scrollView.mj_insetL;
        
        / / record UIScrollView
        _scrollView = (UIScrollView *)newSuperview;
        // The vertical spring effect is always supported
        _scrollView.alwaysBounceVertical = YES;
        // Record the initial contentInset of UIScrollView
        _scrollViewOriginalInset = _scrollView.mj_inset;
        
        // Add a listener
        [selfaddObservers]; }}Copy the code

The newSuperview is first filtered so that only UIScrollView and its subclasses can proceed further.

Then, remove the old listener first.

If the operation is to remove the current view, the if code below is skipped and the method is finished. If you are adding the current view to the superview (newSuperview), save some values, and finally add a new listener.

Let’s look at adding and removing listeners:

- (void)addObservers
{
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentOffset options:options context:nil];
    [self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentSize options:options context:nil];
    self.pan = self.scrollView.panGestureRecognizer;
    [self.pan addObserver:self forKeyPath:MJRefreshKeyPathPanState options:options context:nil];
}

- (void)removeObservers
{
    [self.superview removeObserver:self forKeyPath:MJRefreshKeyPathContentOffset];
    [self.superview removeObserver:self forKeyPath:MJRefreshKeyPathContentSize];
    [self.pan removeObserver:self forKeyPath:MJRefreshKeyPathPanState];
    self.pan = nil;
}
Copy the code

The listening is mainly for the contentOffset of self.scrollView (the parent view), contentSize, and the panGestureRecognizer state of the parent view.

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    // Return: cannot interact
    if (!self.userInteractionEnabled) return;
    
    // This needs to be handled even if it is invisible
    if ([keyPath isEqualToString:MJRefreshKeyPathContentSize]) {
        [self scrollViewContentSizeDidChange:change];
    }
    
    / / see nothing
    if (self.hidden) return;
    
    if ([keyPath isEqualToString:MJRefreshKeyPathContentOffset]) {
        [self scrollViewContentOffsetDidChange:change];
    } else if ([keyPath isEqualToString:MJRefreshKeyPathPanState]) {
        [selfscrollViewPanStateDidChange:change]; }}Copy the code

When listening to the transformation, will trigger the corresponding treatment methods respectively (3) below, including scrollViewContentOffsetDidChange: next pull on refresh and load is used much more, only two methods on the pull back loaded more commonly used.

- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change{}
- (void)scrollViewPanStateDidChange:(NSDictionary *)change{}
Copy the code

Next is a set of public methods:

(1) set the callback object and callback methods, provides an internal response mode, which USES the target – the Action way, refresh the callback executeRefreshingCallback when used, see below the internal method.

- (void)setRefreshingTarget:(id)target refreshingAction:(SEL)action
{
    self.refreshingTarget = target;
    self.refreshingAction = action;
}

#pragmaMark - Internal method

- (void)executeRefreshingCallback
{
    MJRefreshDispatchAsyncOnMainQueue({
        
        if (self.refreshingBlock) {
            self.refreshingBlock();
        }
        
        // #define MJRefreshMsgSend(...) ((void (*)(void *, SEL, UIView *))objc_msgSend)(__VA_ARGS__)
        // #define MJRefreshMsgTarget(target) (__bridge void *)(target)
        if ([self.refreshingTarget respondsToSelector:self.refreshingAction]) {
            MJRefreshMsgSend(MJRefreshMsgTarget(self.refreshingTarget), self.refreshingAction, self);
        }
        
        if (self.beginRefreshingCompletionBlock) {
            self.beginRefreshingCompletionBlock(); }})}Copy the code

Next comes an important setter that subclasses can override to update the refresh control whenever the state method changes.

- (void)setState:(MJRefreshState)state { _state = state; / / to join the home side column is the purpose of the setState: after completion of method calls, set the text to layout child controls MJRefreshDispatchAsyncOnMainQueue ([self setNeedsLayout]) }Copy the code

Each case provides a method with a block and a method without a block. The latter saves the block and calls the former, which is used to add additional operations after the end of the refresh.

#pragmaMark enters the refresh state

- (void)beginRefreshing
{
    [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
        self.alpha = 1.0;
    }];
    self.pullingPercent = 1.0;
    // Display fully as long as you are refreshing
    if (self.window) {
        self.state = MJRefreshStateRefreshing;
    } else {
        // Call this method in case the header inset fails to be backloaded while refreshing
        if (self.state ! = MJRefreshStateRefreshing) {self.state = MJRefreshStateWillRefresh;
            // Refresh (in case you go back to this controller from another controller, refresh it again)
            [selfsetNeedsDisplay]; }}} - (void)beginRefreshingWithCompletionBlock:(void(^) (void))completionBlock
{
    self.beginRefreshingCompletionBlock = completionBlock;
    
    [self beginRefreshing];
}

#pragmaMark Finishes refreshing the status

- (void)endRefreshing
{
    MJRefreshDispatchAsyncOnMainQueue(self.state = MJRefreshStateIdle;)
}

- (void)endRefreshingWithCompletionBlock:(void(^) (void))completionBlock
{
    self.endRefreshingCompletionBlock = completionBlock;
    
    [self endRefreshing];
}
Copy the code

(3) The last method is to automatically change the transparency according to the drag progress, that is, if you need to automatically change the transparency, during the drag process, the drag progress will always be assigned to self.alpha.

#pragmaMark automatically toggles transparency

- (void)setAutoChangeAlpha:(BOOL)autoChangeAlpha
{
    self.automaticallyChangeAlpha = autoChangeAlpha;
}

- (BOOL)isAutoChangeAlpha
{
    return self.isAutomaticallyChangeAlpha;
}

- (void)setAutomaticallyChangeAlpha:(BOOL)automaticallyChangeAlpha
{
    _automaticallyChangeAlpha = automaticallyChangeAlpha;
    
    if (self.isRefreshing) return;
    
    if (automaticallyChangeAlpha) {
        self.alpha = self.pullingPercent;
    } else {
        self.alpha = 1.0; }}#pragmaMark sets the transparency based on the drag progress

- (void)setPullingPercent:(CGFloat)pullingPercent
{
    _pullingPercent = pullingPercent;
    
    // The pullingPercent value is not being refreshed and is required to automatically change transparency to alpha, otherwise it will not proceed further.
    
    if (self.isRefreshing) return;
    
    if (self.isAutomaticallyChangeAlpha) {
        self.alpha = pullingPercent; }}Copy the code

MJRefreshHeader

MJRefreshHeader is a subclass of MJRefreshComponent, but is not yet ready for final use. Let’s take a look at the two constructors it provides, one using block and the other target-action, that hold the callback to the response while creating the instance object.

#pragmaMark - constructor

+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock
{
    MJRefreshHeader *cmp = [[self alloc] init];
    cmp.refreshingBlock = refreshingBlock;
    return cmp;
}

+ (instancetype)headerWithRefreshingTarget:(id)target refreshingAction:(SEL)action
{
    MJRefreshHeader *cmp = [[self alloc] init];
    [cmp setRefreshingTarget:target refreshingAction:action];
    return cmp;
}
Copy the code

Prepare and ‘ ‘set the key, its height, and its y-coordinate mj_y. There appeared a ignoredScrollViewContentInsetTop, speculation is a reserved refreshHeader and clearance between tableView value, the default is 0, requires the user to set just can have value.

- (void)prepare
{
    [super prepare];
    
    / / set the key
    self.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey;
    
    // Set the height
    self.mj_h = MJRefreshHeaderHeight;
}

- (void)placeSubviews
{
    [super placeSubviews];
    
    // Set the y value (when your height changes, you must adjust the y value, so in the placeSubviews method to set the y value)
    self.mj_y = - self.mj_h - self.ignoredScrollViewContentInsetTop;
}
Copy the code

Then the two important methods, scrollViewContentOffsetDidChange: and setState:.

scrollViewContentOffsetDidChange: The method mainly updates self.state based on the current state (self.state) and contentOffset, which is taken into account to avoid frequently updating the value of state, as described in the code comments.

setState: The scrollView () function adds a refreshHeader height to the scrollView contentinset. top, and subtracts the height of the refreshHeader. To control the floating state of the refresh control. In addition, when recovering from the refresh, the current moment is saved, which is used to display the last refresh time.

Finally is two public methods, one is used to capture the last refresh stored in the local time, the other is a ignoredScrollViewContentInsetTop setter, update the refresh control y value at the same time.

MJRefreshStateHeader

The MJRefreshStateHeader is also a part of this inheritance, inheriting from the MJRefreshStateHeader, where the utility phase begins:

  • In the prepare method, save the labels corresponding to the three states to a variable dictionary (stateTitles) for later presentation.
- (void)prepare {
    [super prepare];

    // Initialize the spacing
    self.labelLeftInset = MJRefreshLabelLeftInset;

    // Initialize the text
    // The state in which the refresh was not triggered initially
    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderIdleText] forState:MJRefreshStateIdle];
    // Drag state
    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderPullingText] forState:MJRefreshStatePulling];
    // Refresh the status
    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderRefreshingText] forState:MJRefreshStateRefreshing];
}

- (void)setTitle:(NSString *)title forState:(MJRefreshState)state {
    if (title == nil) return;
    self.stateTitles[@(state)] = title;
    self.stateLabel.text = self.stateTitles[@(self.state)];
}
Copy the code
  • Add two labels for header in lazy loading mode, which are used to display the status prompt text and the last refresh time respectively.
/** Displays the refreshed status of the label */
- (UILabel *)stateLabel {
    if(! _stateLabel) { [self addSubview:_stateLabel = [UILabel mj_label]];
    }
    return _stateLabel;
}

/** Displays the last refresh time of the label */
- (UILabel *)lastUpdatedTimeLabel {
    if(! _lastUpdatedTimeLabel) { [self addSubview:_lastUpdatedTimeLabel = [UILabel mj_label]];
    }
    return _lastUpdatedTimeLabel;
}
Copy the code
  • insetState:StateLabel is assigned to state titles by state titles. LastUpdatedTimeLabel is assigned to lastUpdatedTimeLabelsetLastUpdatedTimeKey:Method is assigned to lastUpdatedTimeLabel after formatting the time, so it should be called once every time the state is updatedself.lastUpdatedTimeKey = self.lastUpdatedTimeKey.
- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    // Set the status text
    self.stateLabel.text = self.stateTitles[@(state)];
    // Reset key (redisplay time)
    self.lastUpdatedTimeKey = self.lastUpdatedTimeKey;
}
Copy the code

MJRefreshGifHeader

MJRefreshGifHeader inherits from the previous class and adds an imageView for displaying animated images and a dictionary of animated images and animation times for each state.

__unsafe_unretained UIImageView *_gifView;

/** All state corresponding animation picture */
@property (strong.nonatomic) NSMutableDictionary *stateImages;
/** The animation time corresponding to all states */
@property (strong.nonatomic) NSMutableDictionary *stateDurations;
Copy the code

In order to retrieve these animated images and times, and associate them with the corresponding state, two methods are provided for external calls. The image must be provided, and the time can not be passed (the second method). The default value images.count * 0.1 is used.

- (void)setImages:(NSArray *)images duration:(NSTimeInterval)duration forState:(MJRefreshState)state 
{ 
    if (images == nil) return; 
    
    self.stateImages[@(state)] = images; 
    self.stateDurations[@(state)] = @(duration); 
    
    /* Set the height of the control according to the picture */ 
    UIImage *image = [images firstObject]; 
    if (image.size.height > self.mj_h) { 
        self.mj_h = image.size.height; }} - (void)setImages:(NSArray *)images forState:(MJRefreshState)state 
{ 
    [self setImages:images duration:images.count * 0.1 forState:state]; 
}
Copy the code

This overrides the setState: method of the parent class, which will set the animation picture only when dragging or refreshing. First stop the previous animation, and then set the new value. If it is a single image, display the animation directly. If state is normal, stop the animation.

- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    
    // Do things according to the state
    if (state == MJRefreshStatePulling || state == MJRefreshStateRefreshing) {
        NSArray *images = self.stateImages[@(state)];
        if (images.count == 0) return;
        
        [self.gifView stopAnimating];
        if (images.count == 1) { // Single image
            self.gifView.image = [images lastObject];
        } else { // Multiple images
            self.gifView.animationImages = images;
            self.gifView.animationDuration = [self.stateDurations[@(state)] doubleValue];
            [self.gifView startAnimating]; }}else if (state == MJRefreshStateIdle) {
        [self.gifView stopAnimating]; }}Copy the code

MJChiBaoZiHeader

Take a look at the implementation of Prepare, which overwrites only one method of its parent class:

- (void)prepare
{
    // 0. Execute the prepare method of the parent class
    [super prepare];
    
    // 1. Set the normal state of the animation picture
    NSMutableArray *idleImages = [NSMutableArray array];
    for (NSUInteger i = 1; i<=60; i++) {
        UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"dropdown_anim__000%zd", i]];
        [idleImages addObject:image];
    }
     [self setImages:idleImages forState:MJRefreshStateIdle];
    
    // 2. Set the state of the animated picture to refresh (the state will refresh when released)
    NSMutableArray *refreshingImages = [NSMutableArray array];
    for (NSUInteger i = 1; i<=3; i++) {
        UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"dropdown_loading_0%zd", i]];
        [refreshingImages addObject:image];
    }
    [self setImages:refreshingImages forState:MJRefreshStatePulling];
    
    // 3. Set the animated picture in the refreshing state
    [self setImages:refreshingImages forState:MJRefreshStateRefreshing];
}
Copy the code

The prepare and setImages: forState: methods come from the base class, as described above.

4.3 Pull-up Loading more Controls (refreshFooter)

Like header, let’s look at the class inheritance:

Since both inherit from MJRefreshComponent, we’ll start with MJRefreshFooter directly, and then proceed to MJChiBaoZiFooter, which is the footer used in Example 2.

MJRefreshFooter

Looking at the source code of this class, you can see that it has many similarities with MJRefreshHeader, such as providing two constructors, one using block and one using target-Action. Here are the main differences.

There is a property called automaticallyHidden that automatically shows and hides footers based on data, but it is not recommended and may be removed later. However, or fake list introduction.

@property (assign.nonatomic.getter=isAutomaticallyHidden) BOOL automaticallyHidden
Copy the code

This property is used in viewWillMoveToSuperView: to set whether or not the footer is hidden, as described in 4.1.

Finally, look at the two public methods, the obsolete one is not listed.

/** hint no more data */
- (void)endRefreshingWithNoMoreData {
    MJRefreshDispatchAsyncOnMainQueue(self.state = MJRefreshStateNoMoreData;)
}

/** Resets no more data (eliminates no more data state) */
- (void)resetNoMoreData {
    MJRefreshDispatchAsyncOnMainQueue(self.state = MJRefreshStateIdle;)
}
Copy the code

MJRefreshAutoFooter

MJRefreshAutoFooter is a direct subclass of MJRefreshFooter.

  • Provides several public properties, whose functions are described in the comments below.
#pragmaMark - Public attributes

/** Whether to refresh automatically (default is YES, that is, the refresh will start automatically when certain trigger conditions are reached) */
@property (assign.nonatomic.getter=isAutomaticallyRefresh) BOOL automaticallyRefresh;

/** Automatically refreshes when the number of bottom controls is present (default is 1.0, i.e., when the bottom controls are completely present) */
@property (assign.nonatomic) CGFloat triggerAutomaticallyRefreshPercent;

/** Whether to send only one request per drag, dragging repeatedly without leaving the screen will not trigger multiple refresh */
@property (assign.nonatomic.getter=isOnlyRefreshPerDrag) BOOL onlyRefreshPerDrag;

#pragmaMark - Private property

/** is a new drag */
@property (assign.nonatomic.getter=isOneNewPan) BOOL oneNewPan;
Copy the code
  • Rewrite thewillMoveToSuperViewMethod, when added to the parent control, give a scrollView. ContentInset. The height of the bottom to add a footer itself, on the contrary, when removed from the parent control, and will add to lose before.
- (void)willMoveToSuperview:(UIView *)newSuperview {
    [super willMoveToSuperview:newSuperview];
    
    if (newSuperview) { // Add to the parent control
        
        if (self.hidden == NO) {
            self.scrollView.mj_insetB += self.mj_h;
        }
        self.mj_y = _scrollView.mj_contentH; // Set the location
        
    } else { 			// is removed
        
        if (self.hidden == NO) {
            self.scrollView.mj_insetB -= self.mj_h; }}}Copy the code
  • These initial values are set during the data preparation phase:
- (void)prepare {
    [super prepare];
    
    // By default, the bottom control is refreshed only when 100% appears
    self.triggerAutomaticallyRefreshPercent = 1.0;
    
    // Set to the default state
    self.automaticallyRefresh = YES;
    
    // The default is to send the request when offset reaches the condition (continuous).
    self.onlyRefreshPerDrag = NO;
}
Copy the code
  • Here are three events triggered by listening on a scrollView:

When the contentSize of the scrollView changes, timing updates the y value of the footer to make sure it stays close to the bottom edge of the scrollView.

- (void)scrollViewContentSizeDidChange:(NSDictionary *)change {
    [super scrollViewContentSizeDidChange:change];
    
    // Set the location
    self.mj_y = self.scrollView.mj_contentH;
}
Copy the code

When sliding scrollView generates contentOffset, the control can only refresh when the bottom refresh control is fully present.

- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
    [super scrollViewContentOffsetDidChange:change];
    
    if (self.state ! = MJRefreshStateIdle || !self.automaticallyRefresh || self.mj_y == 0) return;
    
    if (_scrollView.mj_insetT + _scrollView.mj_contentH > _scrollView.mj_h) { // The content is more than one screen
        // Replace self.mj_y with _scrollview.mj_contenth here
        if (_scrollView.mj_offsetY >= _scrollView.mj_contentH - _scrollView.mj_h + self.mj_h * self.triggerAutomaticallyRefreshPercent + _scrollView.mj_insetB - self.mj_h) {
            // Prevent continuous calls when the hand is released
            CGPoint old = [change[@"old"] CGPointValue];
            CGPoint new = [change[@"new"] CGPointValue];
            if (new.y <= old.y) return;
            
            // Refresh only when the bottom refresh control is fully present
            [selfbeginRefreshing]; }}}Copy the code

When gestures when there is a change of state, UIGestureRecognizerStateEnded UIGestureRecognizerStateBegan and two state for processing, for the former, Depending on when contentoffset. y decides to start refreshing, the latter considers it the start of a new gesture.

- (void)scrollViewPanStateDidChange:(NSDictionary *)change
{
    [super scrollViewPanStateDidChange:change];
    
    if (self.state ! = MJRefreshStateIdle)return;
    
    UIGestureRecognizerState panState = _scrollView.panGestureRecognizer.state;
    if (panState == UIGestureRecognizerStateEnded) {/ / the hand
        if (_scrollView.mj_insetT + _scrollView.mj_contentH <= _scrollView.mj_h) {  // Not enough for one screen
            if (_scrollView.mj_offsetY >= - _scrollView.mj_insetT) { / / pulled up
                [selfbeginRefreshing]; }}else { // Go beyond one screen
            if (_scrollView.mj_offsetY >= _scrollView.mj_contentH + _scrollView.mj_insetB - _scrollView.mj_h) {
                [selfbeginRefreshing]; }}}else if (panState == UIGestureRecognizerStateBegan) {
        self.oneNewPan = YES; }}Copy the code
  • And of course they rewrite itsetStateMethod, if the state is refreshed, to perform the refresh callback; If it goes from the flush state to a state where there is no more data or the flush has stopped, the block that stopped the flush completes is executed.
- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    
    if (state == MJRefreshStateRefreshing) {
        
        // Refresh the state, perform the refresh callback
        [self executeRefreshingCallback];
        
    } else if (state == MJRefreshStateNoMoreData || state == MJRefreshStateIdle) {
        
        If there is a completed callback, execute it from the refresh state to no more data or normal state
        if (MJRefreshStateRefreshing == oldState) {
            if (self.endRefreshingCompletionBlock) {
                self.endRefreshingCompletionBlock(); }}}}Copy the code
  • Finally,setHidden:Method, according to show hidden changes, adjustcontentInsetstateself.frame.origin.y.
- (void)setHidden:(BOOL)hidden
{
    BOOL lastHidden = self.isHidden;
    
    [super setHidden:hidden];
    
    // Change from show to hide
    if(! lastHidden && hidden) {self.state = MJRefreshStateIdle;
        self.scrollView.mj_insetB -= self.mj_h;
        
    } else if(lastHidden && ! hidden) {// Change from hidden to displayed
        
        self.scrollView.mj_insetB += self.mj_h;
        // Set the location
        self.mj_y = _scrollView.mj_contentH; }}Copy the code

MJRefreshAutoStateFooter

MJRefreshAutoStateFooter inherits from MJRefreshAutoFooter, which is similar to MJRefreshStateHeader, and can be used directly in the UI. Only the differences from MJRefreshStateHeader are covered here.

  • Only one shows the refresh statusstateLabelAnd a mutable dictionary that holds copy in different statesstateTitles
/** Displays the refreshed status of the label */
__unsafe_unretained UILabel *_stateLabel;

/** All state corresponding text */
@property (strong.nonatomic) NSMutableDictionary *stateTitles;
Copy the code
  • Rewrite the parent classprepareMethod, in addition to saving the localization copy of various states, it also adds the click gesture to stateLabel.
- (void)prepare
{
    [super prepare];
    
    // Initialize the spacing
    self.labelLeftInset = MJRefreshLabelLeftInset;
    
    // Initialize the text
    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshAutoFooterIdleText] forState:MJRefreshStateIdle];
    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshAutoFooterRefreshingText] forState:MJRefreshStateRefreshing];
    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshAutoFooterNoMoreDataText] forState:MJRefreshStateNoMoreData];
    
    / / to monitor the label
    self.stateLabel.userInteractionEnabled = YES;
    [self.stateLabel addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(stateLabelClick)]];
}
Copy the code

When you click stateLabel, if it is normal and not refreshed, it starts to refresh.

- (void)stateLabelClick {
    if (self.state == MJRefreshStateIdle) {
        [selfbeginRefreshing]; }}Copy the code
  • rewritesetState:Method when added to refresh process pairsstateLabelShow and hide control.
- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    
    if (self.isRefreshingTitleHidden && state == MJRefreshStateRefreshing) {
        self.stateLabel.text = nil;
    } else {
        self.stateLabel.text = self.stateTitles[@(state)]; }}Copy the code

MJRefreshAutoGifFooter

MJRefreshAutoGifFooter inherits from MJRefreshAutoStateFooter, and if you look closely at its implementation code, you will see that it is very similar to MJRefreshGifHeader, It’s basically a gifView (UIImageView) on top of the parent class to display an animated image, and pretty much everything else, Just added MJRefreshStateNoMoreData with no more data and explicit and implicit control of gifView and stateLabel.

MJChiBaoZiFooter

In MJChiBaoZiFooter, the prepare method of the parent class is also rewritten, and the setImages: forState: method of the parent class is called to set the animation picture in the innovation state. For the method implementation, see the parent class.

- (void)prepare
{
    [super prepare];
    
    // Set the animated picture that is being refreshed
    NSMutableArray *refreshingImages = [NSMutableArray array];

    for (NSUInteger i = 1; i<=3; i++) {
        UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"dropdown_loading_0%zd", i]];
        [refreshingImages addObject:image];
    }

    [self setImages:refreshingImages forState:MJRefreshStateRefreshing];
}
Copy the code

5. Summary

This article is just a brief discussion of the MJRefresh source code, many of the details are not fully covered, and will be updated in due course.

6. Reference

  • Effective Objective-C 2.0
  • Summary of iOS 11 security zone adaptation