Turn a Callback Function into a Promise in Objective-C and Java

I have been working on a React Native wrapper library for the Zendesk Support SDK. The SDK uses functions that accept a callback to upload a file. I wanted to upload an array of files and when all of the files were open, I wanted to call another function to create the Zendesk ticket. I know how to do this in JavaScript but it took me a while to figure out how to do it in Objective-C and Java, since the languages use different terms. In this blog post, I’ll show how I would accomplish the same task in all three languages.

The Zendesk SDK

If you’re not familiar with it, Zendesk is a system for managing support tickets that your users submit. Any time you fill out a “Contact Support” form, there’s a good chance the company is using Zendesk to see the tickets and manage the responses.

This support form consists of three fields: a subject, a description of the problem, and an optional array of files (screenshots) that the user would like to submit.

In the Zendesk SDK, you must first upload any files. That function returns a response with an id token. Then you create the ticket using the subject, description, and an array of the responses that you got from uploading the files.

JavaScript

I didn’t actually write the JavaScript example for this implementation because, in the real project, everything was done using native code. However, I wanted to show a JavaScript version so that readers could compare across all three languages.

So for this blog post, I’ll use a couple of fake Zendesk SDK functions for the JavaScript example. They’re based on the real ones but I’ll simplify them to make the examples easier to read. The functions include:

  1. ZendeskSdkUploadFile – a function that takes a file name and callback function. The callback is called with response and error parameters.
  2. ZendeskSdkCreateRequest – a function that takes a subject string, a description string, and an optional array of responses that were returned by uploadFile.

In JavaScript, the normal practice is to create a wrapper function around the SDK function. This wrapper returns a new promise and then the callback function is used to resolve or reject the promise.

function uploadFile(fileName) {
    return new Promise((resolve, reject) => {
        ZendeskSdkUploadFile(fileName, (response, error) => {
            if (error) {
                reject(error);
                return;
            }
            resolve(response);
        });
    });
}
 
function createRequest(subject, description, files) {
    return new Promise((resolve, reject) => Promise.all(files.map(uploadFile)).then((responses) =>
        ZendeskSdkCreateRequest(
            subject,
            description,
            responses,
            (response, error) => {
                if (error) {
                    reject(error);
                    return;
                }
                resolve(response);
            }
        )
    ));
}
 
createRequest(
    "subject",
    "description", 
    ["file1.png", "file2.png"]
).then(console.log);

In this code example, you can see how my uploadFile function wraps the ZendeskSdkUploadFile function with a promise and then uses the callback function to resolve or reject the promise.

The createRequest function is similar but a little more complicated. It also returns a promise; however, the function inside uses a .map to turn an array of files into an array of promises (the promise that was returned by uploadFile). It then uses a Promise.all to wait for those files to be uploaded and then calls our mock ZendeskSdkCreateRequest function with the responses. ZendeskSdkCreateRequest also takes a callback function which is used to resolve or reject the top-level promise.

So that’s how I would handle this situation in JavaScript, but I needed to do the same in Objective-C and Java in my React Native wrapper functions.

Objective-C

I have not written a lot of Objective-C so this solution took me quite a while to figure out. It seems like Objective-C doesn’t naturally lend itself to asynchronous programming like JavaScript.

RCT_EXPORT_METHOD(createRequest:(NSString *)subject description:(NSString *)description attachments:(NSArray<NSString *> *)filePaths resolve:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
 
    /* variable declarations deleted for clarity */
 
    NSMutableArray *uploadResponses = [NSMutableArray array];
    ZDKUploadProvider *upload = [ZDKUploadProvider new];
 
    dispatch_group_t fileUploadGroup = dispatch_group_create();
    for (NSString *filePath in filePaths) {
        dispatch_group_enter(fileUploadGroup);
 
        /* get info about file using filePath */
 
        [upload uploadAttachment:imageData withFilename:fileName andContentType:fileExtension callback:^(ZDKUploadResponse *uploadResponse, NSError *error) {
            if (error) {
                reject(/* error message */);
            } else {
                [uploadResponses addObject:uploadResponse];
                dispatch_group_leave(fileUploadGroup);
            }
        }];
    }
 
    dispatch_group_notify(fileUploadGroup, dispatch_get_main_queue(), ^{
        ZDKRequestProvider *provider = [ZDKRequestProvider new];
        ZDKCreateRequest *request = [ZDKCreateRequest new];
        /* add data to request */
 
        [provider createRequest:request withCallback:^(ZDKDispatcherResponse *result, NSError *error) {
            if (error) {
                reject(/* error message */);
            } else {
                resolve(/* response object */);
            }
        }];
    });
}

There were a number of constraints involved in this solution. I considered several solutions that involved uploading the files individually using JavaScript to control the native SDK. This would have made it easy to wrap the uploader in a promise but the issue was that creating the ticket requires the full response from the file upload process. That file upload response includes the entire binary of the image. So if we used JS as the control, then you’d end up passing the entire image across the React Native bridge which is not very efficient. You’d also have to serialize and deserialize the full response class which is pretty complicated.

So the best solution seemed to be to do all of the uploading and ticket creation in one step in the native code. In Objective-C, the way to perform asynchronous actions seems to be using dispatch queues.

Reading the code, the first thing you’ll notice is the RCT_EXPORT_METHOD. This code is part of a React Native bridge module so that macro is used to declare a function that will be exposed to the JavaScript side of React Native. You can also see that the function accepts a subject, description, an array of file paths, and resolver/rejecter functions that will be used to close the promise on the JavaScript side.

You’ll see a few comments about some deleted code. This was just to shorten the example a little. These variables are mostly related to parsing the file name & extension from the file path and creating a file handler that points to the actual file in memory.

Next we get to the meat of the asynchronous code. First I create a fileUploadGroup. This is the equivalent of my JavaScript array of promises. Then I loop through my array of file paths. At the start of each loop, I use dispatch_group_enter to add to my dispatch queue. Then I upload the file to Zendesk, and if the upload is successful, I use dispatch_group_leave to signify to my dispatch queue that that piece of the queue is complete.

After I’ve started my uploads, I use dispatch_group_notify to wait for when the queue has finished processing. This is the equivalent of my JavaScript Promise.all. Once my dispatch queue is complete, I’ll create a new Zendesk request, attach my data, and upload it. Then in the callback, I resolve/reject to close the promise in JavaScript.

Java

The Java version of this problem was also pretty challenging for me. I’d never used CompletableFutures and it took some time to figure out the syntax. CompletableFutures seem more powerful than JavaScript promises but that comes with increased complexity as well.

@ReactMethod 
public void createRequest(final String subject, final String description, ReadableArray filePaths, final Promise promise) { 
    try { 
        final List<CompletableFuture<String>> uploadFutures = new ArrayList<>(); 
        for(int i = 0; i < filePaths.size(); i++) {
            uploadFutures.add(
                this.uploadImage(filePaths.getString(i))
            );
        } 
        CompletableFuture<Void> allFutures = CompletableFuture.allOf(uploadFutures.toArray(new CompletableFuture[uploadFutures.size()])); 
        CompletableFuture<List<String>> allUploadsFuture = allFutures.thenApply(new Function<Void, List<String>>() { 
            @Override 
            public List<String> apply(Void v) { 
                List<String> result = new ArrayList<>(); 
                for (int i = 0; i < uploadFutures.size(); i++) { 
                    result.add(uploadFutures.get(i).join()); 
                } 
                return result;
            } 
        }); 
        allUploadsFuture.thenAccept(new Consumer<List<String>>() { 
            @Override 
            public void accept(List<String> uploadResponses) { 
                RequestProvider provider = Support.INSTANCE.provider().requestProvider(); 
                CreateRequest request = new CreateRequest();
                /* set request data */
 
                provider.createRequest(request, new ZendeskCallback<Request>() { 
                    @Override 
                    public void onSuccess(Request request) {    
                        promise.resolve(/* result */); 
                    } 
                    @Override 
                    public void onError(ErrorResponse errorResponse) { 
                        promise.reject(/* error */); 
                    } 
                }); 
            } 
        }).exceptionally(new Function<Throwable, Void>() { 
            @Override 
            public Void apply(Throwable t) { 
                promise.reject(/* error */); 
                return null; 
            } 
        }); 
    } catch (IllegalViewOperationException e) {
        promise.reject(/* error */); 
    } 
} 
 
private CompletableFuture<String> uploadImage(String filePath) { 
    final UploadProvider uploadProvider = Support.INSTANCE.provider().uploadProvider(); 
    /* other variables for file handling */ 
 
    final CompletableFuture<String> uploadFuture = new CompletableFuture<>();
    uploadProvider.uploadAttachment(fileName, fileToUpload, mimeType, new ZendeskCallback<UploadResponse>() { 
        @Override 
        public void onSuccess(UploadResponse uploadResponse) { 
            uploadFuture.complete(
                uploadResponse.getToken()
            ); 
        } 
        @Override 
        public void onError(ErrorResponse errorResponse) { 
            uploadFuture.completeExceptionally(/* error */); 
        } 
    }); 
    return uploadFuture; 
}

The Java version feels more similar to something I might write in JavaScript. It involves creating a List of CompletableFutures, waiting for that list to process, and then using the thenApply to create the ticket once all of the uploads have finished.

Similar to the Objective-C function, the Java version starts with @ReactMethod to indicate that this is a function that is exposed to the JavaScript side of React Native. You’ll note that this createRequest function also takes a subject, description and array of file paths. It also accepts a promise which is used to communicate with the JavaScript promise.

I start off by creating a list for my uploadFutures. This list is built using a for loop to call the uploadImage function on each file path. uploadImage creates a CompletableFuture, uploads the file using the Zendesk SDK, resolves the future in the callback using complete and returns that future. In JavaScript this would be the equivalent of building the array of promises.

The next step in createRequest is to convert my list of uploadFutures into a single CompletableFuture so that I can wait for all of them to resolve. Finally, I use the thenAccept to wait for my uploads to finish, create my Zendesk request, attach the data, and submit the request. Once the ticket has been submitted, I reject/resolve the promise that was passed into the function to tell the JavaScript code that the ticket is complete.

Conclusion

It’s been an interesting exercise to try to implement the same feature in 3 different languages. I’m sure the Objective-C and Java versions could be simplified or cleaned up a little if I knew those languages better but they work well for my purposes.

Because we didn’t take the time to wrap the entire Zendesk Support SDK, we chose not to open source this wrapper library. It’s specifically tailored to the project I was working on for Virta Health.

Leave a Comment