I'm currently trying to set up a custom delegate on an async dataTaskWithRequest via NSURLSession. I've set up the protocol and implemented the delegate method, but I am stuck at figuring out whether I've implemented it correctly, and how to unit test it. Specifically, I'd like to test if the delegate returns something after it has been called, and test with a live API call. I've tried testing via the approach suggested here (OCUnit test for protocols/callbacks/delegate in Objective-C), but the test fails, probably because I'm missing something or am not taking into account the async call. Code of attempted delegate implementation and unit test are below.
Delegate protocol declaration:
#include "PtvApiPublic.h"
#ifndef PtvApiDelegate_h
#define PtvApiDelegate_h
#class PtvApi;
#protocol PtvApiDelegate <NSObject>
-(void) ptvApiHealthCheck: (PtvApi *) sender;
#end
#endif /* PtvApiDelegate_h */
Header file:
#include "PtvApiDelegate.h"
#ifndef PtvApi_h
#define PtvApi_h
#interface PtvApi : NSObject
#property (nonatomic, weak) id <NSURLSessionDelegate> delegate;
- (void)ptvApiHealthCheck;
#end
#endif /* PtvApi_h */
Snippet of PtvApi.m
#import <Foundation/Foundation.h>
#import <CommonCrypto/CommonHMAC.h>
#import <CommonCrypto/CommonDigest.h>
#import "PtvApiPublic.h"
#import "PtvApiPrivate.h"
#import "PtvApiDelegate.h"
#implementation PtvApi
#synthesize delegate;
...
- (void)ptvApiHealthCheck
{
NSString *fullUrl = [self GenerateRequestUrl];
NSURLSession *apiSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:delegate delegateQueue:nil];
NSURL *apiUrl = [NSURL URLWithString: fullUrl];
NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:apiUrl];
[apiSession dataTaskWithRequest:urlRequest];
}
#end
Unit test:
#import <XCTest/XCTest.h>
#import "PtvApiPublic.h"
#interface APIDelegateTests : XCTestCase <NSURLSessionDelegate>
{
PtvApi *testApi;
BOOL callbackInvoked;
}
#end
#implementation APIDelegateTests
- (void)setUp {
[super setUp];
testApi = [[PtvApi alloc] init];
testApi.delegate = self;
}
- (void)tearDown {
testApi.delegate = nil;
[super tearDown];
}
- (void)testThatApiCallbackWorks {
[testApi ptvApiHealthCheck];
XCTAssert(callbackInvoked, #"Delegate should return something, I think...");
}
#end
Alright I figured it out myself with the help of Apple's documentation for NSURLSession and the blog at http://www.infinite-loop.dk/blog/2011/04/unittesting-asynchronous-network-access/. I've tested it with the live URL for now, I'll update the answer when I add API mocking to my unit test and make the unit test more robust.
The short of it is that NSURLSession has its own delegate methods, and in this case where dataTaskWithRequest is used, an NSURLSessionDataDelegate can be set up and used to retrieve the API results.
The code for the delegate declaration is mostly correct, I just needed to change NSURLSessionDelegate to NSURLSessionDataDelegate in the header file.
The unit test requires a bit of set up, but is otherwise pretty straightforward. It involves initialising the class with the NSURLSession call, setting the object's delegate to self, and initialise a flag variable to NO. The flag variable will be set to YES when the delegate is called, which is what I am initially testing for. The fully set up unit test is below.
#interface APIDelegateTests : XCTestCase <NSURLSessionDataDelegate>
{
PtvApi *testApi;
BOOL callbackInvoked;
}
#end
#implementation APIDelegateTests
- (void)setUp {
[super setUp];
testApi = [[PtvApi alloc] init];
testApi.delegate = self;
callbackInvoked = NO;
}
- (void)tearDown {
testApi.delegate = nil;
[super tearDown];
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
callbackInvoked = YES;
}
// Method is credit to Claus Brooch.
// Retrieved from http://www.infinite-loop.dk/blog/2011/04/unittesting-asynchronous-network-access/ on 10/04/2016
- (BOOL)waitForCompletion:(NSTimeInterval)timeoutSecs {
NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:timeoutSecs];
do {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:timeoutDate];
if([timeoutDate timeIntervalSinceNow] < 0.0)
break;
} while (!callbackInvoked);
return callbackInvoked;
}
- (void)testThatApiCallbackWorks {
[testApi ptvApiHealthCheck];
XCTAssert([self waitForCompletion:30.0], #"Testing to see what happens here...");
}
#end
Related
I have some Realm models in my app that all use a base class. In this class I wrote some generic functions like the one below:
- (void)save {
self.updatedAt = [NSDate date];
[self.realm beginWriteTransaction];
[self.realm addOrUpdateObject:self];
[self.realm commitWriteTransaction];
[[SyncEngine sharedInstance] store:self];
}
Now, I also wrote a class called SyncEngine, which checks if some available synchronization methods are enabled and then calls them:
- (void)store:(id)object {
if ([Preferences CloudKitEnabled]) {
[self.cloudKit store:object];
}
}
This is where my problem arises. I have written a base class called CloudKitManager which has some generic functions. I then create a specific CloudKitClass for every model in my app, so I'll end up with CloudKitRestaurant and CloudKitTable. All of these will contain a function (void)store:(id)sender. What would be the best way to call the store function of a specific CloudKit class, based on the class that is being stored in Realm?
Ideally, I'd like for RLMRestaurant to automatically use CloudKitRestaurant and not have to use and if else or switch statement.
For further clarity, this is how SyncEngine works.
#interface SyncEngine()
#property (nonatomic, strong) CloudKitManager *cloudKitManager;
#end
#implementation SyncEngine
static SyncEngine *sharedInstance = nil;
+ (SyncEngine *)sharedInstance {
if (sharedInstance == nil) {
sharedInstance = [[self alloc] init];
}
return sharedInstance;
}
- (instancetype)init {
self = [super init];
if (self) {
self.cloudKitManager = [[CloudKitManager alloc] init];
}
return self;
}
#end
In my opinion, you should keep type of CloudKitManager class inside RLMBase object. And when you need to call [[CloudKitManager sharedInstance] store:object], call [[object.cloudKitClass sharedInstance] store:object].
Try my code below.
#interface RLMBase : NSObject
- (Class)cloudKitClass;
#end
#implementation RLMBase
- (Class)cloudKitClass {
// Must be overridden in subclass.
return CloudKitManager.class;
}
#end
#interface RLMRestaurant : RLMBase
#end
#implementation RLMRestaurant
- (Class)cloudKitClass {
return CloudKitRestaurant.class;
}
#end
- (void)store:(RLMBase *)object {
if ([Preferences CloudKitEnabled]) {
[[object.cloudKitClass sharedInstance] store:object];
}
}
ANOTHER WAY
Put store: method from SyncEngine to RLMBase object.
#interface RLMBase : NSObject
- (Class)cloudKitClass;
- (void)store;
#end
#implementation RLMBase
- (Class)cloudKitClass {
// Must be overridden in subclass.
return CloudKitManager.class;
}
- (void)store {
if ([Preferences CloudKitEnabled]) {
[[self.cloudKitClass sharedInstance] store:self];
}
}
#end
And save method will become
- (void)save {
self.updatedAt = [NSDate date];
[self.realm beginWriteTransaction];
[self.realm addOrUpdateObject:self];
[self.realm commitWriteTransaction];
[self store];
}
I have created a class that will collect data from url data asynchronously, however my understanding of callbacks or whatever is not clear and I'm trying to find a simple way to reuse my class by having the calling method wait for data to be returned or set within the ApiManager class. I just need something to wakeup in another class when that process has been completed. Some processes have single request and others have multiple, why you will notice that I'm using [connection description] within the ApiManager class.
ApiManager.h
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#interface ApiManager : NSObject<NSURLConnectionDelegate>
{
NSMutableDictionary *_dataDictionary;
}
- (void)urlRequest:(NSURLRequest *)url;
#property (strong, nonatomic) NSMutableArray *results;
#end
ApiManager.m
#import "ApiManager.h"
#implementation ApiManager
- (void)urlRequest:(NSURLRequest *)url {
[[NSURLConnection alloc] initWithRequest:url delegate:self];
}
// basic connection classes
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
NSMutableData *responceOjb = _dataDictionary[ [connection description] ];
[_dataDictionary setObject:responceOjb forKey:[connection description]];
}
// append any data we find
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
NSMutableData *responceOjb = _dataDictionary[ [connection description] ];
[responceOjb appendData: data];
[_dataDictionary setObject:responceOjb forKey:[connection description]];
}
// --
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection
willCacheResponse:(NSCachedURLResponse*)cachedResponse {
// Return nil to indicate not necessary to store a cached response for this connection
return nil;
}
// wrap up and close the connect, move objects over to results or something
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[_results addObject:[connection description]];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
// The request has failed for some reason!
// Check the error var
NSLog(#"%#",error);
}
#end
The main View Controller test:
#import "ViewController.h"
#import "ApiManager.h"
#interface ViewController ()
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self DoThisTest];
}
-(void)DoThisTest {
ApiManager *api = [[ApiManager alloc] init];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:#"%#",#"http://google.com"]]];
[api urlRequest:request];
if([api results]) {
NSLog(#"GOT DATA");
}
}
Well, there are a few options. You could add a block property onto your ApiManager class:
#property (copy, nonatomic) void (^doneHandler)();
And then invoke that block like so:
self.doneHandler();
You would invoke the block when you deem it appropriate (say, in your connectionDidFinishLoading: method).
With this approach, the definition of the block (callback) would happen in your view controller and look something like:
ApiManager *apiManager = [[ApiManager alloc] init];
apiManager.doneHandler = ^{
// Do whatever you need to do here.
};
Alternatively, you could add a method to your ApiManager with a signature like this:
- (void)sendRequestWithURL:(NSURL*)url completion:(void(^)())completion;
And use NSURLConnection's (or, better, NSURLSession's) block-based APIs. Those APIs have callbacks built in and you would simply invoke completion(); inside of the completion block of -[NSURLSession sendAsynchronousRequest:completion:].
Finally, you could define an ApiManagerDelegate protocol.
- (void)apiManagerDidFinishReceivingData:(ApiManager*)sender;
And add a delegate property to your ApiManager class.
#property (weak, nonatomic) id<ApiManagerDelegate>delegate;
Assign the delegate of your ApiManager in your ViewController:
ApiManager *apiManager = [[ApiManager alloc] init];
apiManager.delegate = self;
Call the delegate method inside of your implementation of NSURLConnectionDelegate's callbacks in ApiManager like so:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[_results addObject:[connection description]];
[self.delegate apiManagerDidFinishReceivingData:self];
}
And implement the delegate method in ViewController:
- (void)apiManagerDidFinishReceivingData:(ApiManager*)sender {
// Do what you want to.
}
As an addendum, there are networking libraries available that do a lot of the heavy lifting and busy-work for you, most notably AFNetworking, if you're just trying to get stuff done. And, even if this is more of an academic exercise where you're trying to understand the patterns, looking at AFNetworking's APIs and implementation (it's open source) would be highly instructive.
Cheers
My goal is to achieve synchronized communication to custom Device i.e. next command can be send only when reply is received. Now I'm doing it in this way
Device class implements DeviceDelegate protocol
//Device.h
#class Device;
#protocol DeviceDelegate <NSObject>
- (void)didReciveReplyWithData:(NSData *)data;
#end
#interface Device : NSObject {}
In DeviceViewController implementation:
#interface DeviceViewController()
{
BOOL waitingForReply = false;
}
#end
#implementation DeviceViewController
- (void)sendCommandWithData:(NSData *)data
{
if ( waitingForReply == false)
{
//send command code
waitingForReply = true;
}
}
- (void)didReciveReplyWithData:(NSData *)data
{
//code
waitingForReply = false;
}
#end
but I wish to do it in more elegant way i.e. by using GCD (semaphores?) with blocks (completionHandler?). Any ideas?
PS. Sorry, but I forgot to mention: all commands sended to device while
waitingForReply = true
should be ignored!!!.
Possibly the best approach here would be to create a queue of commands with NSOperationQueue.
Since, presumably, the communication with the device is asynchronous you will have to subclass NSOperation to encapsulate the communication.
#interface DeviceCommandOperation : NSOperation <DeviceDelegate>
#property (nonatomic, assign) BOOL waitingForReply;
#property (nonatomic, copy) NSData *dataToSend;
#property (nonatomic, copy) NSData *dataReceived;
#end
#implementation DeviceCommandOperation
- (instancetype)initWithData:(NSData *)dataToSend
{
self = [super init];
if (self)
{
_dataToSend = [dataToSend copy];
}
return self;
}
- (void)setWaitingForReply:(BOOL)waitingForReply
{
if (_waitingForReply != waitingForReply)
{
[self willChangeValueForKey:#"isExecuting"];
[self willChangeValueForKey:#"isFinished"];
_waitingForReply = waitingForReply;
[self didChangeValueForKey:#"isExecuting"];
[self didChangeValueForKey:#"isFinished"];
}
}
- (void)start
{
self.waitingForReply = YES;
// Simulate sending a command and waiting for response.
// You will need to replace this with your actual communication mechanism.
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// In reality this call would presumably come from the Device
[self didReceiveReplyWithData:someData];
});
}
- (void)didReceiveReplyWithData:(NSData *)data
{
self.dataReceived = data;
self.waitingForReply = NO;
}
#pragma mark - NSOperation
- (BOOL)isAsynchronous
{
return YES;
}
- (BOOL)isExecuting
{
return _waitingForReply;
}
- (BOOL)isFinished
{
return !_waitingForReply;
}
#end
This operation could then be used from your DeviceViewController (it would probably be better architecturally to have this responsibility elsewhere but that's not the topic of this question).
#interface DeviceViewController ()
#property (nonatomic, strong) NSOperationQueue *operationQueue;
#end
#implementation DeviceViewController
- (NSOperationQueue *)operationQueue
{
if (_operationQueue == nil)
{
_operationQueue = [[NSOperationQueue alloc] init];
}
return _operationQueue;
}
- (void)sendNextCommand
{
NSData *data = // Get data for the next command
[self sendCommandWithData:data];
}
- (void)sendCommandWithData:(NSData *)data
{
NSLog(#"Queueing operation");
DeviceCommandOperation *operation = [[DeviceCommandOperation alloc] initWithData:data];
// The operation's completionBlock gets called on a background queue
[operation setCompletionBlock:^{
NSLog(#"DeviceCommandOperation completed");
// Process operation.dataReceived
[self sendNextCommand];
}];
[self.operationQueue addOperation:operation];
}
#end
This approach will allow you to determine what (if any) command to send next, based on the reply to the previous command.
If you know all of the "commands" you will want to send initially and don't need finer grained control you could create instances of DeviceCommandOperation for each command, set the queue's maxConcurrentOperationCount to 1, and add each DeviceCommandOperation to the queue (in the order you want them to be processed).
I am getting the use of undeclared identifier 'data' in the didreceiveresponse method. I have imported the Model header file which has the array and data declared. I found that if I declare them inside the viewcontroller.h file the error goes away. What is the cause of this problem?
#import "ViewController.h"
#import "DetailViewController.h"
//#import "Model.h"
#import "Model.h"
#interface ViewController ()
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
NSURL *url = [NSURL URLWithString:#"https://s3.amazonaws.com/jon-hancock-phunware/nflapi-static.json"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[[NSURLConnection alloc] initWithRequest:request delegate:self];
// Do any additional setup after loading the view, typically from a nib.
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
data = [[NSMutableData alloc]init];
}
This is the model header file.
#import <UIKit/UIKit.h>
#interface Model : UIViewController
//NSArray *bars;
//NSMutableData *data;
#property (nonatomic, retain) NSArray *bars;
#property (nonatomic, retain) NSMutableData *data;
#end
This fixes the error defining the variables inside the viewcontroller.h file.
import
#interface ViewController : UIViewController {
IBOutlet UITableView *mainTableView;
NSArray *bars;
NSMutableData *data;
}
#end
It's Objective-C not Swift. You access properties with self.data or _data. It doesn't know well enough to look to your global variables unless you're explicit about it. Also, I would lean towards self.data over directly accessing _data so the setter gets called. In this case, it's almost certainly not going to be an issue but setting ivars directly with an underscore can mess up key value observation and might have unexpected consequences that are difficult to debug.
This question already has answers here:
How do I create delegates in Objective-C?
(20 answers)
Closed 9 years ago.
On iOS, how do I create a delegate (user defined)?
First define a declare a delegate like this -
#protocol IconDownloaderDelegate;
Then create a delegate object like this -
#interface IconDownloader : NSObject
{
NSIndexPath *indexPathInTableView;
id <IconDownloaderDelegate> delegate;
NSMutableData *activeDownload;
NSURLConnection *imageConnection;
}
Declare a property for it -
#property (nonatomic, assign) id <IconDownloaderDelegate> delegate;
Define it -
#protocol IconDownloaderDelegate
- (void)appImageDidLoad:(NSIndexPath *)indexPath;
#end
Then you can call methods on this delegate -
[delegate appImageDidLoad:self.indexPathInTableView];
Here is the complete source code of the image downloader class -
.h file -
#class AppRecord;
#class RootViewController;
#protocol IconDownloaderDelegate;
#interface IconDownloader : NSObject
{
AppRecord *appRecord;
NSIndexPath *indexPathInTableView;
id <IconDownloaderDelegate> delegate;
NSMutableData *activeDownload;
NSURLConnection *imageConnection;
}
#property (nonatomic, retain) AppRecord *appRecord;
#property (nonatomic, retain) NSIndexPath *indexPathInTableView;
#property (nonatomic, assign) id <IconDownloaderDelegate> delegate;
#property (nonatomic, retain) NSMutableData *activeDownload;
#property (nonatomic, retain) NSURLConnection *imageConnection;
- (void)startDownload;
- (void)cancelDownload;
#end
#protocol IconDownloaderDelegate
- (void)appImageDidLoad:(NSIndexPath *)indexPath;
#end
.m file -
#import "IconDownloader.h"
#import "MixtapeInfo.h"
#define kAppIconHeight 48
#define TMP NSTemporaryDirectory()
#implementation IconDownloader
#synthesize appRecord;
#synthesize indexPathInTableView;
#synthesize delegate;
#synthesize activeDownload;
#synthesize imageConnection;
#pragma mark
- (void)dealloc
{
[appRecord release];
[indexPathInTableView release];
[activeDownload release];
[imageConnection cancel];
[imageConnection release];
[super dealloc];
}
- (void)startDownload
{
self.activeDownload = [NSMutableData data];
// alloc+init and start an NSURLConnection; release on completion/failure
NSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:
[NSURLRequest requestWithURL:
[NSURL URLWithString:appRecord.mixtape_image]] delegate:self];
self.imageConnection = conn;
[conn release];
}
- (void)cancelDownload
{
[self.imageConnection cancel];
self.imageConnection = nil;
self.activeDownload = nil;
}
#pragma mark -
#pragma mark Download support (NSURLConnectionDelegate)
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[self.activeDownload appendData:data];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
// Clear the activeDownload property to allow later attempts
self.activeDownload = nil;
// Release the connection now that it's finished
self.imageConnection = nil;
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
// Set appIcon and clear temporary data/image
UIImage *image = [[UIImage alloc] initWithData:self.activeDownload];
self.appRecord.mixtape_image_obj = image;
self.activeDownload = nil;
[image release];
// Release the connection now that it's finished
self.imageConnection = nil;
// call our delegate and tell it that our icon is ready for display
[delegate appImageDidLoad:self.indexPathInTableView];
}
#end
and here is how we use it -
#import "IconDownloader.h"
#interface RootViewController : UITableViewController <UIScrollViewDelegate, IconDownloaderDelegate>
{
NSArray *entries; // the main data model for our UITableView
NSMutableDictionary *imageDownloadsInProgress; // the set of IconDownloader objects for each app
}
in .m file -
- (void)startIconDownload:(AppRecord *)appRecord forIndexPath:(NSIndexPath *)indexPath
{
IconDownloader *iconDownloader = [imageDownloadsInProgress objectForKey:indexPath];
if (iconDownloader == nil)
{
iconDownloader = [[IconDownloader alloc] init];
iconDownloader.appRecord = appRecord;
iconDownloader.indexPathInTableView = indexPath;
iconDownloader.delegate = self;
[imageDownloadsInProgress setObject:iconDownloader forKey:indexPath];
[iconDownloader startDownload];
[iconDownloader release];
}
}
here is delegate gets called automatically -
// called by our ImageDownloader when an icon is ready to be displayed
- (void)appImageDidLoad:(NSIndexPath *)indexPath
{
IconDownloader *iconDownloader = [imageDownloadsInProgress objectForKey:indexPath];
if (iconDownloader != nil)
{
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:iconDownloader.indexPathInTableView];
// Display the newly loaded image
cell.imageView.image = iconDownloader.appRecord.appIcon;
}
}
This is basic concepts to create a own delegate
Delegates are very useful to control transfer within the array of view controllers in app manually. Using delegates you can manage the control flow very well.
here is small example of own delegates....
Create a protocol class.... (.h only)
SampleDelegate.h
#import
#protocol SampleDelegate
#optional
#pragma Home Delegate
-(NSString *)getViewName;
#end
Import above protocol class in the class whom you want to make delegate of another class. Here in my ex. I m using AppDelegate to make delegate of The HomeViewController's Object.
also add above DelegateName in Delegate Reference < >
ownDelegateAppDelegate.h
#import "SampleDelegate.h"
#interface ownDelegateAppDelegate : NSObject <UIApplicationDelegate, SampleDelegate> {
}
ownDelegateAppDelegate.m
//setDelegate of the HomeViewController's object as
[homeViewControllerObject setDelegate:self];
//add this delegate method definition
-(NSString *)getViewName
{
return #"Delegate Called";
}
HomeViewController.h
#import
#import "SampleDelegate.h"
#interface HomeViewController : UIViewController {
id<SampleDelegate>delegate;
}
#property(readwrite , assign) id<SampleDelegate>delegate;
#end
HomeViewController.h
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
UILabel *lblTitle = [[UILabel alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
lblTitle.text = [delegate getViewName];
lblTitle.textAlignment = UITextAlignmentCenter;
[self.view addSubview:lblTitle];
}