Singleton pattern with parameter - objective-c

in my iPhone application, I'm using a subclass of AFHTTPClient to access a rest web service. I want all my requests to be handled by one instance of my API client so I use a singleton pattern.
This works fine when the service is running only on once URL. I can use a constant value to set the URL.
Now, in the final version of the application, each app will actually talk to another service that will be installed in the corporate network.
So I will be getting the service URL from a remote configuration. Is the singleton pattern still a good choice here? How am I supposed to parameterise it if the URL could actually even change during the runtime of the app ?
cheers
#import "FooAPIClient.h"
#import "AFJSONRequestOperation.h"
static NSString * const kFooAPIBaseURLString = #"http://192.168.0.1";
#implementation FooAPIClient
+ (instancetype)sharedClient {
static FooAPIClient *_sharedClient = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedClient = [[self alloc] initWithBaseURL:[NSURL URLWithString:kFooAPIBaseURLString]];
});
return _sharedClient;
}
- (id)initWithBaseURL:(NSURL *)url {
self = [super initWithBaseURL:url];
if (!self) {
return nil;
}
[self registerHTTPOperationClass:[AFJSONRequestOperation class]];
[self setDefaultHeader:#"Accept" value:#"application/json"];
return self;
}
#end

This could be the solution. Instead of subclassing the AFHTTPClient, just set it as property and re-instantiate it if the URL changes:
#import "FooAPIClient.h"
#import "AFJSONRequestOperation.h"
#import "AFHTTPClient.h"
static NSString * const kFooAPIBaseURLString = #"http://192.168.0.1";
#interface FooAPIClient ()
#property AFHTTPClient * httpClient;
#end
#implementation FooAPIClient
+ (instancetype)sharedClient {
static FooAPIClient *_sharedClient = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedClient = [[self alloc] initWithBaseURL:[NSURL URLWithString:kFooAPIBaseURLString]];
});
return _sharedClient;
}
- (id)initWithBaseURL:(NSURL *)url {
self = [super init];
if (!self) {
self.httpClient = [self setupClientForURL:url];
}
return self;
}
-(AFHTTPClient*) setupClientForURL:(NSURL*) url {
AFHTTPClient * httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
[httpClient registerHTTPOperationClass:[AFJSONRequestOperation class]];
[httpClient setDefaultHeader:#"Accept" value:#"application/json"];
return httpClient;
}
#pragma mark - RemoteConfigurationDelegate
-(void) apiURLChanged:(NSURL*) newURL {
self.httpClient = [self setupClientForURL:newURL];
}
#pragma mark - Public
-(void) consumeAPI:(CompletionBlock) completion {
[self.httpClient getPath:#"foo" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
if(completion) {
completion(responseObject, nil);
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
if(completion) {
completion(nil, error);
}
}];
}
#end

The singleton pattern doesn't have to be a straitjacket.
This lesson I learned from Cocoa Touch. There are several classes in the framework that use shared instances, but allow you the flexibility of creating your own instances if you need them. NSNumberFormatter, NSDateFormatter, NSBundle, NSFileManager and many many others are examples of classes where you can create your own instances if you need them.
In your case, I would have two class methods that return instances:
+ (instancetype)sharedClient {
static FooAPIClient *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] initWithBaseURL:[NSURL URLWithString:kFooAPIBaseURLString]];
});
return instance;
}
static FooAPIClient *FooSharedCorporateInstance;
+ (instancetype)sharedCorporateClient {
#syncronized (FooSharedCorporateInstance) {
return FooSharedCorporateInstance;
}
}
+ (void)setSharedCorporateClientWithURL:(NSURL *)URL {
#syncronized (FooSharedCorporateInstance) {
FooSharedCorporateInstance = [[self alloc] initWithBaseURL:URL];
}
}
As a side benefit, this forces separate class and instance responsibilities that tend to blur in singleton classes.

Related

Subclassing a singleton not working as expected

I'm writing a class that communicates with an API. I started by creating a SessionManager:
#interface SessionManager : AFHTTPSessionManager
+ (id)sharedManager;
#end
static NSString *const kBaseURL = #"https://myapi.com";
#implementation SessionManager
- (id)init {
self = [super initWithBaseURL:[NSURL URLWithString:kBaseURL]];
if(!self) return nil;
self.responseSerializer = [AFJSONResponseSerializer serializer];
self.requestSerializer = [AFJSONRequestSerializer serializer];
return self;
}
+ (id)sharedManager {
static SessionManager *_sessionManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sessionManager = [[self alloc] init];
});
return _sessionManager;
}
#end
I wrote several classes that extend this SessionManager. One for every entity in the API: RestaurantManager, StreetManager and AreaManager. What happens, though, is that when I use one of them and then another, it will still use the first one.
NSArray *restaurants = [[RestaurantManager sharedManager] getRestaurants];
// restaurants contains all the restaurants
NSArray *streets = [[StreetsManager sharedManager] getStreets];
// streets still contains all the restaurants
Any ideas as to how to fix this?
You should override + (id)sharedManager methods in subclasses. Otherwise, they go to the same sharedManager method and interact with the same static SessionManager variable.

EXC_BAD_ACCESS for an object created inside a Block

I have always been nervous when it comes to blocks and GCD because my mind tells me that it looks very complex!
I am getting a crash inside a block which ideally looks alright to me:
#pragma mark -
-(void)fetchOrdersListWithInfoDict:(NSDictionary*)infoDict completionBlock:(CompletionBlock)completionBlock errorBlock:(ErrorBlock)errorBlock
{
__weak VTVehicleServiceNetworkManager *weakSelf = self;
TaskBlock fetchOrdersListTaskBlock = ^()
{
__block __strong HLOrdersDataProvider *ordersDataProvider = nil;
NSBlockOperation *fetchOrdersOperation = [NSBlockOperation blockOperationWithBlock:[^{
ordersDataProvider = [[HLOrdersDataProvider alloc] init];
[ordersDataProvider performFetchOrdersListWithInfoDict:infoDict
completionBlock:completionBlock
errorBlock:errorBlock];
} copy]];
[weakSelf.dataOperationQueue addOperation:fetchOrdersOperation];
};
[self fetchDataWithTaskBlock:[fetchOrdersListTaskBlock copy]
errorBlock:^(NSError *error) {
errorBlock(error);
}];
}
I was able to trace out the zombie object but I am not able to figure out why is this object turning out to be a zombie. Here is the snapshot from profile:
I have gone through the following guides (1, 2) to see if I can find out what I am doing wrong but I was no where near to find out what is going wrong.
Any help and reference text to what I am doing wrong will help.
Edit:
I have tried what #Jerimy has suggested and in fact my code which I have posted earlier was exactly the same as required: Declaring and initializing ordersDataProvider inside the block operation itself. But since it was crashing at the same point I tried to declare it outside the block just to see if it addresses the crash.
Below is the new code I tested:
#pragma mark -
-(void)fetchOrdersListWithInfoDict:(NSDictionary*)infoDict completionBlock:(CompletionBlock)completionBlock errorBlock:(ErrorBlock)errorBlock
{
__weak VTVehicleServiceNetworkManager *weakSelf = self;
completionBlock = [completionBlock copy];
errorBlock = [errorBlock copy];
TaskBlock fetchOrdersListTaskBlock = ^()
{
NSBlockOperation *fetchOrdersOperation = [NSBlockOperation blockOperationWithBlock:^{
HLOrdersDataProvider *ordersDataProvider = [[HLOrdersDataProvider alloc] init];
[ordersDataProvider performFetchOrdersListWithInfoDict:infoDict
completionBlock:completionBlock
errorBlock:errorBlock];
}];
[weakSelf.dataOperationQueue addOperation:fetchOrdersOperation];
};
[self fetchDataWithTaskBlock:[fetchOrdersListTaskBlock copy]
errorBlock:^(NSError *error) {
errorBlock(error);
}];
}
The crash from Profile:
There is not much from the stack trace as well, SDMHTTPRequest is a library and am very sure there is nothing wrong there, and the HLOrdersDataProvider is the zombie object which I was able to trace out in Instruments app:
EDIT 2
Adding the interface and implementation of HLOrdersDataProvider for more details:
#interface HLOrdersDataProvider : HLDataProvider
-(void)performFetchOrdersListWithInfoDict:(NSDictionary*)infoDict completionBlock:(CompletionBlock)completionBlock errorBlock:(ErrorBlock)errorBlock;
#end
#implementation HLOrdersDataProvider
-(void)performFetchOrdersListWithInfoDict:(NSDictionary*)infoDict completionBlock:(CompletionBlock)completionBlock errorBlock:(ErrorBlock)errorBlock
{
// Using SDMConnectivity
NSString *queryString = [infoDict valueForKey:#"$filter"];
NSString *appendStringForEndpoint = [kRowsetsKeyword stringByAppendingFormat:#"?%#", queryString];
[self fetchDataFromServerWithEndPointAppendString:appendStringForEndpoint
completionBlock:completionBlock
errorBlock:errorBlock];
}
#pragma mark - Service Agent related
-(NSString*)collectionName
{
return [NSString stringWithString:kRowsetsKeyword];
}
-(void)requestFinished:(SDMHttpRequest *)request
{
NSError *error = nil;
// Let's parse the response and send the results back to the caller
NSString *collectionName = [self collectionName];
NSData *responseData = [request responseData];
NSArray *entitiesArray = [self parseODataEntriesWithData:responseData
withCollectionName:collectionName
error:&error];
if (error)
[self triggerFailureBlockWithArgument:error];
else
[self triggerCompletionBlockWithArgument:entitiesArray];
}
#end
Also, HLOrdersDataProvider is inherited from HLDataProvider so below is the interface and implementation of this class too:
#import <Foundation/Foundation.h>
//#import "SDMHttpRequestDelegate.h"
#import "SDMRequestBuilder.h"
#import "SDMHttpRequest.h"
#import "SDMParser.h"
#import "HLConstant.h"
#import "HLConnectionData.h"
#interface HLDataProvider : NSObject <SDMHttpRequestDelegate>
#property (copy, atomic) CompletionBlock completionBlock;
#property (copy , atomic) ErrorBlock errorBlock;
#property (copy, atomic) CompletionBlockWithDataFetchStatus completionBlockWithDataFetchStatus;
+ (id)sharedInstance;
- (NSMutableArray*)parseODataEntriesWithData:(NSData*)data withCollectionName:(NSString*)collectionName;
- (NSMutableArray*)parseODataEntriesWithData:(NSData*)data withCollectionName:(NSString*)collectionName error:(NSError**)outError;
- (NSMutableArray*)parseJSONEntriesWithData:(NSData*)data;
-(NSArray*)fetchEntriesFromDatabaseWithEntityName:(NSString*)entityName relationshipObjects:(NSMutableArray*)relationships;
-(void)updateDatabaseWithEntries:(NSMutableArray*)scanEntries;
-(void)updateDatabaseWithJSONEntries:(NSMutableArray*)scanEntries;
-(id)parsedOdataResultFromEntries:(NSMutableArray*)entries;
-(void)fetchDataFromServerWithEndPointAppendString:(NSString*)appendStr completionBlock:(CompletionBlock)inCompletionBlock errorBlock:(ErrorBlock)inErrorBlock;
-(NSString*)collectionName;
-(void)triggerCompletionBlockWithArgument:(id)inArg;
-(void)triggerFailureBlockWithArgument:(NSError*)inArg;
#end
#implementation HLDataProvider
+ (id)sharedInstance
{
//Subclassess will override this method
return nil;
}
-(NSMutableArray*)parseODataEntriesWithData:(NSData*)data withCollectionName:(NSString*)collectionName
{
return [self parseODataEntriesWithData:data
withCollectionName:collectionName
error:NULL];
}
-(NSMutableArray*)parseODataEntriesWithData:(NSData*)data withCollectionName:(NSString*)collectionName error:(NSError**)outError
{
NSMutableArray *entriesArray = nil;
#try {
entriesArray = sdmParseODataEntriesXML(data,
[[[HLConnectionData metaDataDocument] getCollectionByName:collectionName] getEntitySchema],
[HLConnectionData serviceDocument]);
}
#catch (NSException *exception) {
NSLog(#"Got exception: %#", exception);
if (outError)
{
*outError = [NSError errorWithDomain:#"Vehicle Service"
code:-1001
userInfo:[NSDictionary dictionaryWithObject:exception forKey:NSLocalizedDescriptionKey]];
}
}
#finally {
}
return entriesArray;
}
- (NSMutableArray*)parseJSONEntriesWithData:(NSData*)data
{
NSError *error = nil;
id object = [NSJSONSerialization
JSONObjectWithData:data
options:0
error:&error];
NSMutableArray *resultArray = nil;
if(error) { /* JSON was malformed, act appropriately here */ }
if([object isKindOfClass:[NSDictionary class]])
{
resultArray = [NSMutableArray arrayWithObject:object];
}
else if ([object isKindOfClass:[NSArray class]])
{
resultArray = [NSMutableArray arrayWithArray:object];
}
return resultArray;
}
#pragma mark -
#pragma mark - Data Fetch - Server - SDMConnectivity
-(void)fetchDataFromServerWithEndPointAppendString:(NSString*)appendStr completionBlock:(CompletionBlock)inCompletionBlock errorBlock:(ErrorBlock)inErrorBlock;
{
self.errorBlock = inErrorBlock;
self.completionBlock = inCompletionBlock;
id<SDMRequesting> request = nil;
//NSString *clientStr = #"&sap-client=320&sap-language=EN";
NSString *urlStr =[NSString stringWithFormat:#"%#/%#",[HLConnectionData applicationEndPoint], appendStr];
urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
[SDMRequestBuilder setRequestType:HTTPRequestType];
request=[SDMRequestBuilder requestWithURL:[NSURL URLWithString:urlStr]];
[request setUsername:kUserName];
/*Set Password in SDMRequesting object*/
[request setPassword:kPassword];
[request setRequestMethod:#"GET"];
[request setTimeOutSeconds:kTimeoutInterval];
/*set the Delegate. This class must adhere to SDMHttpRequestDelegate to get the callback*/
[request setDelegate:self];
/*Call startAsynchronous API to request object to retreive Data asynchrnously in the call backs */
[request startSynchronous];
}
-(void)updateDatabaseWithEntries:(NSMutableArray*)scanEntries
{
//Subclasses will override this
}
-(void)updateDatabaseWithJSONEntries:(NSMutableArray*)scanEntries
{
//Subclasses will override this
}
-(id)parsedOdataResultFromEntries:(NSMutableArray*)entries
{
//Subclasses will override this
return nil;
}
-(void)deleteExistingEntriesFromCoredata
{
//Subclasses will override this
}
-(NSArray*)fetchEntriesFromDatabaseWithEntityName:(NSString*)entityName relationshipObjects:(NSMutableArray*)array
{
//Subclasses will overide this method
return nil;
}
#pragma mark - SDMHTTPRequestDelegate methods
- (void)requestStarted:(SDMHttpRequest*) request
{
}
- (void)requestFinished:(SDMHttpRequest*) request
{
// For service doc and metadata we instantiate HLDataProvider, so we send this raw SDMHTTPRequest object as-is. For other service agents like HLOrdersDataProvider we send the parsed information, check the subclass' implementation of -requestFinished: method
[self triggerCompletionBlockWithArgument:request];
}
-(void)triggerCompletionBlockWithArgument:(id)inArg
{
self.completionBlock(inArg);
}
- (void)requestFailed:(SDMHttpRequest*) request
{
[self triggerFailureBlockWithArgument:request.error];
}
-(void)triggerFailureBlockWithArgument:(NSError*)inArg
{
self.errorBlock(inArg);
}
- (void)requestRedirected:(SDMHttpRequest*) request
{
}
#pragma mark - Service Agent related
-(NSString*)collectionName
{
// Should be overridden by the subclasses
return nil;
}
The referenced code is very convoluted (you're doing things in a very complicated way).
First off, you should not be creating the copy of your blocks in the caller. Make the copy in the callee if necessary (ie: to copy it to heap instead of using the stack-allocated block if you are going to call it after the stack has been popped). In almost all APIs using blocks, it is not the caller's responsibility to ensure that a block is on heap.
There is no reason for your ordersDataProvider variable to be declared in the scope of fetchOrdersListTaskBlock because it is only ever used inside of fetchOrdersOperation's block.
I don't immediately see the cause of your crash, but I suspect that simplifying your code will help reveal the problem. Perhaps the issue is in HLOrdersDataProvider's initializer.
Try something like:
-(void)fetchOrdersListWithInfoDict:(NSDictionary*)infoDict completionBlock:(CompletionBlock)completionBlock errorBlock:(ErrorBlock)errorBlock
{
completionBlock = [completionBlock copy];
errorBlock = [errorBlock copy];
__weak VTVehicleServiceNetworkManager *weakSelf = self;
TaskBlock fetchOrdersListTaskBlock = ^{
NSBlockOperation *fetchOrdersOperation = [NSBlockOperation blockOperationWithBlock:^{
HLOrdersDataProvider *ordersDataProvider = [[HLOrdersDataProvider alloc] init];
[ordersDataProvider performFetchOrdersListWithInfoDict:infoDict
completionBlock:completionBlock
errorBlock:errorBlock];
}];
[weakSelf.dataOperationQueue addOperation:fetchOrdersOperation];
};
[self fetchDataWithTaskBlock:fetchOrdersListTaskBlock
errorBlock:errorBlock];
}
Or better yet, re-design your class to work like this (I don't see a need for NSBlockOperation in your example):
-(void)fetchOrdersListWithInfoDict:(NSDictionary*)infoDict completionBlock:(CompletionBlock)completionBlock errorBlock:(ErrorBlock)errorBlock
{
completionBlock = [completionBlock copy];
errorBlock = [errorBlock copy];
TaskBlock fetchOrdersListTaskBlock = ^{
HLOrdersDataProvider *ordersDataProvider = [[HLOrdersDataProvider alloc] init];
[ordersDataProvider performFetchOrdersListWithInfoDict:infoDict
completionBlock:completionBlock
errorBlock:errorBlock];
};
[self fetchDataWithTaskBlock:fetchOrdersListTaskBlock
errorBlock:errorBlock];
}

Force deletion of singleton in test code that was created with dispatch_once

I'm writing some unit test code for a model class and want to simulate the behavior of the class during app exit and relaunch. I could achieve this by deleting and re-allocing the object, however its a singleton and thus the following code doesn't have the desired effect:
+ (id) sharedInstance
{
static MyModel *singleton = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^ {
singleton = [[MyModel alloc] initSharedInstance];
});
return singleton;
}
// Test code:
MyModel* gModel = [MyModel sharedInstance];
... tests
gModel = nil;
gModel = [MyModel sharedInstance];
... more tests
Is there a neat solution so I can delete/recreate the object?
static MyModel *singleton = nil;
static dispatch_once_t onceToken;
+ (instancetype) sharedInstance
{
dispatch_once(&onceToken, ^ {
if (singleton==nil){
singleton = [[MyModel alloc] initSharedInstance];
}
});
return singleton;
}
+(void)setSharedInstance:(MyModel *)instance {
onceToken = 0;
singleton = instance;
}
Nil it:
[MyModel setSharedInstance:nil];
Note that you can also set it to an arbitrary class to mock it.
[MyModel setSharedInstance:someMock];
sure something like this would be fine for unit testing, you can turn it off for prod:
static MyModel *singleton = nil;
+ (id) sharedInstance
{
if(!singleton)
{
singleton = [self new];
}
return singleton;
}
+ (void)resetSingleton
{
[singlelton release];
singleton = nil;
}

iOS NSKeyedUnarchiver calling singleton getter

I have a singleton class that will initialize it's data from a web service then save itself to a file (using NSCoding and NSKeyedUnarchiver) so I can just initialize this file instead of pulling all the data from the web again. Because it's a singleton instance that is being saved, I want to be able to call the singleton getter like you normally would, check to see if there's an archived copy and if not, pull the data down and archive it. Archiving is working fine, but when I try to call [[NSKeyedUnarchiver unarchiveObjectWithFile: filePath] retain] it calls the sharedInstance getter before sharedInstance is initialized. This causes init to be called and the app then downloads all the data again, just to be subsequently overwritten by the unarchiver.
Am I doing something wrong with my setup, or is there another way of Serializing this data?
Here's some (simplified) code:
#implementation Helmets
#synthesize helmetList, helmetListVersion;
//Class Fields
static Helmets *sharedInstance = nil;
// Get the shared instance and create it if necessary.
+ (Helmets *)sharedInstance {
//if(sharedInstance == nil){
//[Helmets unarchive]; //This does not work! Calls sharedInstance() again (recursion)
if(sharedInstance == nil){
sharedInstance = [[super allocWithZone:NULL] init]; //Pull from web service
[Helmets archive]; //Save instance
//}
//}
return sharedInstance;
}
- (id)init {
self = [super init];
if (self) {
helmetList = [[NSMutableArray alloc]init];
//Get our data from the web service and save it (excluded)
}
}
return self;
}
//This works!
+(void)archive{
if([NSKeyedArchiver archiveRootObject:sharedInstance toFile:[Helmets getFilePath]]){
NSLog(#"Archiving Successful");
}
else{
NSLog(#"Archiving Failed");
}
}
//This works, but calls getInstance causing data to be downloaded anyways!
+(void)unarchive{
// Check if the file already exists
NSFileManager *filemgr = [NSFileManager defaultManager];
NSString *filePath = [Helmets getFilePath];
if ([filemgr fileExistsAtPath: filePath])
{
sharedInstance = [[NSKeyedUnarchiver unarchiveObjectWithFile: filePath] retain];
}
[filemgr release];
}
Instance is initialized like:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
...
[Helmets unarchive]; //This calls sharedInstance() too soon!
[Helmets sharedInstance];
}
The class implements NSCoding in the .h and overrides initWithCoder and encodeWithCoder (Archiving is working).
Thanks in advance!
In the end you need a private method to set your shared instance in addition to the one you have, and you need a different init, again private to the implementation.
- (id)initAndFetch:(BOOL)fetch
{
if((self = [super init])) {
...
if(fetch) { do the web fetch };
...
}
}
In the +sharedInstance method, you will pass YES.
Then your decode will look like:
- (id)initWithCoder:(NSCoder *)decoder
{
if((self = [self initAndFetch:NO])) {
title = [decoder decodeObjectForKey:#"title"];
...
sharedInstance = self;
}
return self;
}
I have got to make it work the following manner.
-(id)initWithCoder:(NSCoder *)aDecoder
{
self = [super init];
if (!self) {
return nil;
}
UserInfo *user =[UserInfo sharedInstance];
return user;
}
-(void)encodeWithCoder:(NSCoder *)aCoder
{
UserInfo *user =[UserInfo sharedInstance];
[aCoder encodeObject: user.phoneNumber forKey:#"phoneNumber"];
}
-(void)save
{
[[NSUserDefaults standardUserDefaults] setObject:[NSKeyedArchiver archivedDataWithRootObject:[UserInfo sharedInstance]] forKey:#"USER_DATA"];
[[NSUserDefaults standardUserDefaults]synchronize];
}
-(UserInfo*)currentUser
{
NSData *data =[[NSUserDefaults standardUserDefaults] objectForKey:#"USER_DATA"];
UserInfo *savedUser =[NSKeyedUnarchiver unarchiveObjectWithData:data];
UserInfo *user =[UserInfo sharedInstance];
return user;
}

changing AFNetworking baseURL

I am using AFNetworking with the singleton model suggested in their example.
+ (SGStockRoomHTTPClient *)sharedClient
{
static SGStockRoomHTTPClient *_sharedClient = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
NSString *baseUrlString = [[NSUserDefaults standardUserDefaults] stringForKey:#"server_root_url_preference"];
_sharedClient = [[self alloc] initWithBaseURL:[NSURL URLWithString:baseUrlString]];
});
return _sharedClient;
}
- (id)initWithBaseURL:(NSURL *)url {
self = [super initWithBaseURL:url];
if (!self) {
return nil;
}
[self registerHTTPOperationClass:[AFJSONRequestOperation class]];
[self setDefaultHeader:#"Accept" value:#"text/html"];
return self;
}
Initialization is done with a baseURL taken from the user defaults.
My problem is that the baseURL property is read-only. If the user goes to settings and changes the baseURL user default, how can I change it in my client?
Another similar case I have with a need to change the baseURL is an API which requires multiple calls and logic to determine to the right baseURL. And the base url can still change while the app is running (e.g. user changes networking environment requiring a change from local connection to 3G connection via external proxy server.).
I see why the baseURL property is read-only: there are things like networkReachabilityStatus that run in the background and are tied to that setting. This said, it seems fairly easy to have a setBaseURL method that stops monitoring, changes the value, then starts monitoring again...
I guess my design is not right, should I give-up the singleton in this case and re-create the client each time the baseURL should change?
The class AFHTTPClient is designed to work with a single base URL. This is why it is readonly.
Here are some solutions if you have more than one base URL:
in your AFHTTPClient subclass override the property qualifier. Place this inside the #interface: #property (readwrite, nonatomic, retain) NSURL *baseURL;
Don't use AFHTTPClient at all
Create multiple instances of AFHTTPClient
When you create HTTP operations you can override the baseURL. Just set the full URL in the path instead of a relative path.
Hope this helps you.
#interface EFApiClient : AFHTTPSessionManager
#property (nonatomic,assign)BOOL isTestEnvironment ;
+ (instancetype)sharedMClient;
#end
#implementation EFApiClient
+ (instancetype)sharedMClient
{
if ([EFApiClient sharedClient].isTestEnvironment) {
return [EFApiClient sharedTestClient] ;
}
else{
return [EFApiClient sharedClient];
}
}
+ (instancetype)sharedClient
{
static EFApiClient *_sharedMClient = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedMClient = [[EFApiClient alloc] initWithBaseURL:[NSURL URLWithString:#"https://xxx.xxx.com"]];
[EFApiClient clientConfigWithManager:_sharedMClient];
});
return _sharedMClient;
}
+ (instancetype)sharedTestClient
{
static EFApiClient *_sharedMClient = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedMClient = [[EFApiClient alloc] initWithBaseURL:[NSURL URLWithString:#"https://test.xxx.xxx.com"]];
[EFApiClient clientConfigWithManager:_sharedMClient];
});
return _sharedMClient;
}
+ (void)clientConfigWithManager:(EFApiClient *)client
{
AFSecurityPolicy* policy = [[AFSecurityPolicy alloc] init];
[policy setAllowInvalidCertificates:YES];
[policy setValidatesDomainName:NO];
[client setSecurityPolicy:policy];
client.requestSerializer = [AFHTTPRequestSerializer serializer];
client.responseSerializer = [AFHTTPResponseSerializer serializer];
//client.requestSerializer.HTTPMethodsEncodingParametersInURI = [NSSet setWithArray:#[#"POST", #"GET", #"HEAD"]];
client.responseSerializer = [AFJSONResponseSerializer serializer];
client.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:#"application/x-javascript",#"application/json", #"text/json", #"text/html", nil];
}
#end