The first address of this article (Mr Huang Huang Huang Xian Sen’s blog (Thatisawesome. Club)

Business background

Imagine a business scenario where the client sends a request to the Server to submit a task through the/API/COMMIT interface. After receiving the request, the Server returns a Response indicating that the task was successfully submitted. In order to obtain the execution progress of the task, You need to call the/API /query interface periodically to query the execution status of the current task until the task is completed. Based on this, how do we write such a polling request?

Based on the above services, the author encapsulates a PHQueryServer singleton object, which maintains a Timer and a floating point variable progress inside. The Timer will randomly add 0% to 10% to progress every 2 seconds to simulate the processing progress of the Server. One is provided externally

- (void)getCurrentProgressWithCompletion:(void(^) (float currentProgress))completion;
Copy the code

Interface to get the current progress.

// PHQueryServer.h
#import <Foundation/Foundation.h>

@interface PHQueryServer : NSObject

- (void)getCurrentProgressWithCompletion:(void(^) (float currentProgress))completion;

+ (instancetype)defaultServer;


@end

// PHQueryServer.m

#import "PHQueryServer.h"

@interface PHQueryServer(a)

@property (nonatomic.assign.readwrite) float currentProgress;
@property (nonatomic.strong) NSTimer *timer;

@end

@implementation PHQueryServer

+ (instancetype)defaultServer {
    static PHQueryServer *server = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        server = [[PHQueryServer alloc] init];
        [server startProcess];
    });
    return server;
}

- (void)startProcess {
    [self.timer fire];
}

- (NSTimer *)timer {
    if(! _timer) { __weak typeof(self) weakSelf = self;
        _timer = [NSTimer scheduledTimerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) {
            [weakSelf process];
        }];
    }
    return _timer;
}

- (void)process {
    // Simulate Server processing asynchronous tasks
    float c = self.currentProgress;
    self.currentProgress = c + (arc4random() % 10);
    if (self.currentProgress >= 100) {
        self.currentProgress = 100;
        [self.timer invalidate];
        self.timer = nil; }} - (float)currentProgress {
    return [@(_currentProgress) floatValue];
}

- (void)getCurrentProgressWithCompletion:(void(^) (float))completion {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^ {// It takes time to simulate network transmission
        sleep(arc4random() % 3);
        float currentProgress = [self currentProgress];
        // Simulating the network acceptance process takes time
        sleep(arc4random() % 2);
        if (completion) {
            dispatch_async(dispatch_get_main_queue(), ^{ completion(currentProgress); }); }}); }@end

Copy the code

Based on theNSTimer

Considering that you need to poll every once in a while, NSTimer is a good fit. The timer periodically sends a network request and updates the Model after receiving a Response. If the task status is Finished (progress >= 100), the invalidate timer ends the polling. Talk is cheap, show me the code.

// PHTimerQueryHelper.h

#import <Foundation/Foundation.h>

typedef void (^PHQueryTimerCallback)(void);

@interface PHTimerQueryHelper : NSObject

- (void)startQueryWithModel:(PHQueryModel *)queryModel
                   callback:(PHQueryTimerCallback)callback;

@end

// PHTimerQueryHelper.m

#import "PHTimerQueryHelper.h"
#import "PHQueryServer.h"

@interface PHTimerQueryHelper(a)

@property (nonatomic.strong) NSTimer               *queryTimer;
@property (nonatomic.copy  ) PHQueryTimerCallback  callback;
@property (nonatomic.strong) PHQueryModel          *queryModel;

@end

@implementation PHTimerQueryHelper

- (void)startQueryWithModel:(PHQueryModel *)queryModel
                   callback:(PHQueryTimerCallback)callback {
    _callback = callback;
    _queryModel = queryModel;
    [self.queryTimer fire];
}

- (NSTimer *)queryTimer {
    if(! _queryTimer) { __weak typeof(self) weakSelf = self;
        _queryTimer = [NSTimer scheduledTimerWithTimeInterval:3 repeats:YES block:^(NSTimer * _Nonnull timer) {
            [[PHQueryServer defaultServer] getCurrentProgressWithCompletion:^(float currentProgress) {
                if (currentProgress > weakSelf.queryModel.progress) {
                    weakSelf.queryModel.progress = currentProgress;
                    if (weakSelf.callback) {
                        dispatch_async(dispatch_get_main_queue(), ^{ weakSelf.callback(); }); }}// End the polling
                if (currentProgress >= 100) {
                    [weakSelf.queryTimer invalidate];
                    weakSelf.queryTimer = nil; }}]; }]; }return _queryTimer;
}
@end

Copy the code

PHQueryServer will execute the time-consuming sleep() function in the child thread to simulate the network request time, and then call back the current progress to the caller via completion callback in the main thread. The caller will get the progress update progress of the queryModel after the progress is modified. Then call back to the UI layer to update the progress bar, the UI layer code is as follows

// ViewController.h
#import "ViewController.h"
#import "PHQueryServer.h"
#import "PHTimerQueryHelper.h"

@import Masonry;
@import CHUIPropertyMaker;

@interface ViewController(a)

@property (nonatomic.strong) PHQueryModel          *queryModel;
@property (nonatomic.strong) PHTimerQueryHelper    *helper;
@property (nonatomic.strong) UIView                *progressView;
@property (nonatomic.strong) UILabel               *progressLabel;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    [self setupViews];
    [PHQueryServer defaultServer];
    _queryModel = [[PHQueryModel alloc] init];

    // 1. Use the NSTimer timer for polling
    [self queryByTimer];
    
}

- (void)setupViews {
    UIView *progressBarBgView = [[UIView alloc] init];
    [progressBarBgView ch_makeProperties:^(CHViewPropertyMaker *make) {
        make.backgroundColor(UIColor.grayColor);
        make.superView(self.view);
        make.cornerRadius(10);
    } constrains:^(MASConstraintMaker *make) {
        make.centerY.equalTo(self.view);
        make.left.equalTo(self.view).offset(20);
        make.right.equalTo(self.view).offset(- 20);
        make.height.equalTo(@20);
    }];
    
    self.progressView = [[UIView alloc] init];
    [self.progressView ch_makeProperties:^(CHViewPropertyMaker *make) {
        make.backgroundColor(UIColor.greenColor);
        make.cornerRadius(10);
        make.superView(progressBarBgView);
    } constrains:^(MASConstraintMaker *make) {
        make.left.bottom.top.equalTo(progressBarBgView);
        make.width.equalTo(@0);
    }];
    
    self.progressLabel = [[UILabel alloc] init];
    [self.progressLabel ch_makeLabelProperties:^(CHLabelPropertyMaker *make) {
        make.superView(self.progressView);
        make.font([UIFont systemFontOfSize:9]);
        make.textColor(UIColor.blueColor);
    } constrains:^(MASConstraintMaker *make) {
        make.centerY.equalTo(self.progressView);
        make.right.equalTo(self.progressView).offset(- 10);
        make.left.greaterThanOrEqualTo(self.progressView).offset(5);
    }];
}


- (void)queryByTimer {
    __weak typeof(self) weakSelf = self;
    [self.helper startQueryWithModel:self.queryModel callback:^{
        [weakSelf updateProgressViewWithProgress:weakSelf.queryModel.progress];
    }];
}

- (PHTimerQueryHelper *)helper {
    if(! _helper) { _helper = [[PHTimerQueryHelper alloc] init]; }return _helper;
}

- (void)updateProgressViewWithProgress:(float)progress {
    [UIView animateWithDuration:1 animations:^{
        [self.progressView mas_remakeConstraints:^(MASConstraintMaker *make) {
            make.left.top.bottom.equalTo(self.progressView.superview);
            make.width.equalTo(self.progressView.superview.mas_width).multipliedBy(progress / 100.0);
        }];
        [self.view layoutIfNeeded];
    } completion:^(BOOL finished) {
        self.progressLabel.text = [NSString stringWithFormat:@"%.2f", progress];
    }];
}

@end
Copy the code

Using Timer polling seems to have no problem, but considering that the network request is triggered at a regular time, the problem may be that the network request sent first comes back. For example, a network request is sent at 0, another network request is sent at 3, and the network request sent at 3 receives a callback at 4. If a request is sent at 0 s and a callback is received at 5 s, the network request sent before a callback is actually meaningless, because the callback message at 4 s is the latest, and the callback message received at 5 s is outdated. So in the above example, the progress of the callback is compared to the progress of the current queryModel, and the polling result will only be called back if the progress is greater than the current progress. Obviously will waste some network resources, such as sending some meaningless requests, there are solutions, a mark is local to remember the last time the network request variables have callback, if there is no correction, is again the next Timer callback network don’t send the request, but this method will lead to new problems. Timer is set to trigger once for 3 seconds. If the network request is sent at the moment of 0s, but the callback is performed at the moment of 4s, there is still 2s before the next Timer trigger, which belongs to a gap period and nothing will be done, so that the polling update is not timely.

Asynchronous basedNSOperation

You can use NSOperation to send network requests in the main method, update Model in the network request callback, refresh the progress in the completionBlock of NSOperation, and determine that it is complete (progress == 100). If not, a new operation is created and placed in the serial queue.

asynchronousNSOperation

In NSOperation, when the main method completes, it indicates that the task has completed, but the network request is obviously an asynchronous operation, so the main method will return before the network request callback.

  • The semaphore turns an asynchronous request into a synchronous one
  • asynchronousNSOperation

If the semaphore is used for synchronization, dispatch_semaphore_wait will block the current thread until dispatch_semaphore_signal is sent after the network request is callback.

Using asynchronous NSOperation does not.

// PHQueryOperation.h
@interface PHQueryOperation : NSOperation

- (instancetype)initWithQueryModel:(PHQueryModel *)queryModel;

@end

// PHQueryOperation.m

#import "PHQueryOperation.h"
#import "PHQueryServer.h"

@interface PHQueryOperation(a)

@property (nonatomic.assign) BOOL ph_isCancelled;
@property (nonatomic.assign) BOOL ph_isFinished;
@property (nonatomic.assign) BOOL ph_isExecuting;
@property (nonatomic.strong) PHQueryModel *queryModel;

@end

@implementation PHQueryOperation

- (instancetype)initWithQueryModel:(PHQueryModel *)queryModel {
    if (self = [super init]) {
        _queryModel = queryModel;
    }
    return self;
}

- (void)start {
    if (self.ph_isCancelled) {
        self.ph_isFinished = YES;
        return;
    }
    
    self.ph_isExecuting = YES;
    [self startQueryTask];
}

- (void)startQueryTask {
    __weak typeof(self) weakSelf = self;
    [[PHQueryServer defaultServer] getCurrentProgressWithCompletion:^(float currentProgress) {
        weakSelf.queryModel.progress = currentProgress;
        weakSelf.ph_isFinished = YES;
    }];
}

- (void)setPh_isFinished:(BOOL)ph_isFinished {
    [self willChangeValueForKey:@"isFinished"];
    _ph_isFinished = ph_isFinished;
    [self didChangeValueForKey:@"isFinished"];
}

- (void)setPh_isExecuting:(BOOL)ph_isExecuting {
    [self willChangeValueForKey:@"isExecuting"];
    _ph_isExecuting = ph_isExecuting;
    [self didChangeValueForKey:@"isExecuting"];
}

- (void)setPh_isCancelled:(BOOL)ph_isCancelled {
    [self willChangeValueForKey:@"isCancelled"];
    _ph_isCancelled = ph_isCancelled;
    [self didChangeValueForKey:@"isCancelled"];
}

- (BOOL)isFinished {
    return _ph_isFinished;
}

- (BOOL)isCancelled {
    return _ph_isCancelled;
}

- (BOOL)isExecuting {
    return _ph_isExecuting;
}

@end

Copy the code

Based on theGCD

Simple and crude

- (void)queryByGCD {
    __weak typeof(self) weakSelf = self;
    [[PHQueryServer defaultServer] getCurrentProgressWithCompletion:^(float currentProgress) {
            if (currentProgress > weakSelf.queryModel.progress) {
                weakSelf.queryModel.progress = currentProgress;
                [self updateProgressViewWithProgress:weakSelf.queryModel.progress];
                if (currentProgress < 100) {
                    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [weakSelf queryByGCD]; }); }}}]; }Copy the code