AFNetworking synchronous calls (like/unlike)

2019-07-19 04:00发布

问题:

I need to implement like/unlike functionality in app. All API calls are made with AFNetworking and success/errors handlers (ios blocks).

Problem is that when user making many clicks on a button in a short period of time, some of the request are received by server in a wrong sequence and then everything becomes wrong. For example double like or double unlike happens.

Is there a way to send all request via AFNetworking synchronously?

If no what is the best practice to design this kind of API requests?

回答1:

Disabling the button (like the comments suggest) is not a bad idea, especially if you throw a spinner or some UI change to let the user know you are processing the change.

Otherwise, you could just limit the API calls to only allow a single call to be out. If the user presses the button, fire the call and change some boolean or tracking value. If they press the button again hold unto the change state locally but wait for the first callback to come in. If they keep pressing the button just keep track of their change but never fire the response until you receive your notification that the API call has completed (probably with a 10-30 second timeout in case it fails).

Once the call is completed, see if the new value the user wants is different. If it is, send that and prevent future changes from going out (but track them locally), if it is the same (the user pressed the button an even amount of times while your first call was out) then don't send it.

I would even delay the first call by 3 or so seconds and every time they press the button within that time period reset the timer. That way you will not be firing accidental calls unnecessarily (think of it as a coredata save, if you know there may be a few changes you make them all before saving).

The problem with a synchronized queue is if they press the button five times (or more) it will have a pretty long wait queue. Then what if they close the application and your calls are not sent? Then your database has (potentially) inaccurate information.



回答2:

The easiest way to do this, IMHO, is to disable the button just before sending the request. Once you have the response in the success or failure callback, you can make UI changes to give feedback that the user has liked whatever he has liked and you can enable the button again.



回答3:

It strikes me that you have two options:

  1. The simple solution is to give the user positive UI feedback that the button was tapped, such as suggested by Moxy (i.e. a UX that prevents the "hey, I should tap on that again because it doesn't look like I got it last time"), but then disable further interaction with that button until the previous action is complete. Or,

  2. The more complicated solution is to reflect the like/unlike change in the UI immediately and manage the network requests asynchronously (and not just in terms of the threads, but logically, too). If you do this, you will want to keep a weak reference to the previous like/unlike operation (and operation queues are great for this sort of problem) for each like/unlike button, such that when you make new a like/unlike request, you can make its operation dependent upon the prior one (so they happen sequentially), and/or cancel the prior one.



回答4:

AFNetworking operations will return before completion event if you put them in an operation queue. Check this blog post: http://www.dribin.org/dave/blog/archives/2009/05/05/concurrent_operations/

In your case, you need to create an NSOperation subclass similar to the following:

//Header file
@interface LikeOperation : NSOperation
@property (readonly, nonatomic) BOOL isExecuting;
@property (readonly, nonatomic) BOOL isFinished;
+ (instancetype)operationWithCompletionSuccessBlock:(void(^)())onSuccess failure:(void(^)(NSError *anError))onError;
@end

//Implementation file
#import "LikeOperation.h"

typedef void (^SuccessBlock)();
typedef void (^ErrorBlock)(NSError*);
@interface LikeOperation()
@property (readwrite, copy, nonatomic) SuccessBlock onSuccess;
@property (readwrite, copy, nonatomic) ErrorBlock onError;
@property (assign, nonatomic) BOOL isExecuting;
@property (assign, nonatomic) BOOL isFinished;
@property (readwrite, strong, nonatomic) AFHTTPClient *client;
@end
@implementation LikeOperation
static NSString *const kBaseURLString = @"www.example.org";
static NSString *const kURLString = @"www.example.org/like";

- (id)initWithCompletionSuccessBlock:(void (^)())onSuccess failure:(void (^)(NSError *))onError
{
    self = [super init];
    if (self)
    {
        self.onSuccess = onSuccess;
        self.onError = onError;
    }
    return self;
}
+ (instancetype)operationWithCompletionSuccessBlock:(void (^)())onSuccess failure:(void (^)(NSError *))onError
{
    return [[self alloc] initWithCompletionSuccessBlock:onSuccess
                                                failure:onError];
}
- (void)start
{
    if (![NSThread isMainThread])
    {
        [self performSelectorOnMainThread:@selector(start)
                               withObject:nil
                            waitUntilDone:NO];
        return;
    }

    NSString *key = NSStringFromSelector(@selector(isExecuting));
    [self willChangeValueForKey:key];
    self.isExecuting = YES;
    [self didChangeValueForKey:key];


    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSURL *baseURL = [NSURL URLWithString:kBaseURLString];
        self.client = [AFHTTPClient clientWithBaseURL:baseURL];
    });

    NSURL *url = [NSURL URLWithString:kURLString];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];

    AFHTTPRequestOperation *operation = [self.client HTTPRequestOperationWithRequest:request success:^(AFHTTPRequestOperation *operation, id responseObject) {
        self.onSuccess();
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        self.onError(error);
    }];
    [operation start];
}
- (void)finish
{
    NSString *isExecutingKey = NSStringFromSelector(@selector(isExecuting));
    NSString *isFinishedKey = NSStringFromSelector(@selector(isFinished));

    [self willChangeValueForKey:isExecutingKey];
    [self willChangeValueForKey:isFinishedKey];

    self.isExecuting = NO;
    self.isFinished = YES;

    [self didChangeValueForKey:isExecutingKey];
    [self didChangeValueForKey:isFinishedKey];
}
@end

After that, you can put the above operation safely in an NSOperationQueue and set the max concurrent maxConcurrentOperationCount to 1 so that the operations run one after the another. You might also want to explore nsoperation dependencies, as explained in http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/NSOperation_class/Reference/Reference.html

//Code to initialize the operation queue
self.queue = [[NSOperationQueue alloc] init];
self.queue.name = @"Post data queue";
self.queue.maxConcurrentOperationCount = 1;

//perform like
- (void)like
{
    NSOperation *likeOperation = [LikeOperation operationWithCompletionSuccessBlock:^{

    } failure:^(NSError *anError) {

    }];

    [self.queue addOperation:likeOperation];
}


回答5:

For Swift4 I have managed it with queue

import UIKit
import Alamofire

class LikeOperation: Operation {

    private var _isExecuting = false
    private var _finished = false
    private var request:DataRequest? = nil
    private var imageID:String

    typealias completionBlock = ((GeneralResponse<User>?) -> Void)?

    var finishedBlock : completionBlock

    init(imageID:String, completionBlock:completionBlock) {
        self.imageID = imageID
        self.finishedBlock = completionBlock
        super.init()

    }

    override var isExecuting: Bool {
        get {
            return _isExecuting
        } set {
            willChangeValue(forKey: "isExecuting")
            _isExecuting = isExecuting
            didChangeValue(forKey: "isExecuting")
        }
    }


    override var isFinished: Bool {
        get {
            return _finished
        } set {
            willChangeValue(forKey: "isFinished")
            _finished = newValue
            didChangeValue(forKey: "isFinished")
        }
    }

    override func start() {
        if isCancelled {
            isFinished = true

            return
        }

        isExecuting = true

        func completeOperation() {
            isFinished = true
            isExecuting = false

        }
        self.request =  APIClient.insertImageLike(ImageID: self.imageID, completion: { (completion:GeneralResponse<User>?, error) in

            self.finishedBlock?(completion!)
            completeOperation()
        })

    }

    override func cancel() {
        super.cancel()

        if isExecuting {
            isFinished = true
            isExecuting = false
        }

        request?.cancel()

    }


}



  func callAPIToLike (post:Post) {
        guard let id = post.id else {
            self.showAlert(withMessage: ErrorMessages.General.somethingWentWrong)
            return
        }

        AppGlobalManager.sharedInstance.homeScreenLikeAPIQueue.cancelAllOperations()
        let operation = LikeOperation.init(imageID: "\(id)") { (object) in

        }
        AppGlobalManager.sharedInstance.homeScreenLikeAPIQueue.addOperation(operation)

    }