Tutorial on AFNetworking with a Singleton Class for iOS Centralized and Decoupled Networking

When discussing iOS architecture patterns, the Model-View-Controller (MVC) design pattern is excellent for ensuring the longevity and maintainability of an app’s codebase. It promotes the decoupling of classes, making them easy to reuse or replace to meet different needs. This approach maximizes the benefits of Object-Oriented Programming (OOP).

While this iOS application architecture works effectively at the micro level (individual screens or sections of an app), as your app grows, you might find yourself adding similar functions to multiple models. For tasks like networking, shifting common logic from your model classes into singleton helper classes can be a more efficient strategy. This AFNetworking iOS tutorial will guide you on setting up a centralized singleton networking object that, decoupled from micro-level MVC components, can be reused throughout your application with a decoupled architecture.

AFNetworking Tutorial: Centralized and Decoupled Networking with Singleton

Challenges in iOS Networking

Apple has successfully abstracted many complexities of managing mobile hardware through user-friendly iOS SDKs. However, certain areas like networking, Bluetooth, OpenGL, and multimedia processing can become cumbersome due to the SDKs’ emphasis on flexibility. Fortunately, the vibrant iOS developer community has developed high-level frameworks to simplify common use cases, ultimately streamlining application design and structure. A skilled programmer, adhering to iOS app architecture best practices, understands which tools to use, why to use them, and when it’s more appropriate to build custom tools and classes.

AFNetworking exemplifies this concept in networking. As one of the most popular open-source frameworks, it simplifies RESTful API networking and establishes modular request/response patterns with success, progress, and failure completion blocks. This eliminates the need for developer-implemented delegate methods and custom request/connection settings, allowing for swift integration into any class.

Challenges with AFNetworking

While powerful, AFNetworking’s modularity can sometimes lead to fragmented implementations. Common inefficient practices include:

  • Multiple network requests within a single view controller using similar methods and properties.

  • Almost identical requests across multiple view controllers, leading to distributed common variables that can become unsynchronized.

  • Network requests within a class for data unrelated to that class.

These might not pose significant problems for applications with limited views, few API calls, and infrequent changes. However, if you envision long-term growth and updates, you’ll likely encounter the need to handle:

  • API versioning to support multiple app generations.

  • Adding new parameters or modifying existing ones over time to enhance functionality.

  • Implementing entirely new APIs.

Having networking code dispersed throughout your codebase can transform these updates into a nightmare. While defining some parameters statically in a common header can help, even minor changes might still require modifying numerous classes.

Addressing AFNetworking’s Limitations

A solution is to create a networking singleton that centralizes the management of requests, responses, and their parameters.

A singleton object provides a global access point to the resources of its class. They are used in situations where a single point of control is desirable, such as with classes offering general services or resources. You obtain this global instance from a singleton class through a factory method. – Apple

Essentially, a singleton is a class with only one instance throughout the application’s lifecycle. This single instance is easily accessible by any class needing its methods or properties.

Here’s why a singleton is beneficial for networking:

  • Static initialization: Once created, its methods and properties are consistently available to any accessing class, eliminating synchronization issues or data requests from incorrect instances.

  • Rate limiting: It allows you to control API call frequency (e.g., limiting requests to five per second).

  • Centralized static properties: Hostname, port numbers, endpoints, API versions, device types, persistent IDs, screen sizes, etc., are co-located, so changes affect all network requests.

  • Reusability: Common properties can be reused across multiple network requests.

  • On-demand instantiation: The singleton object consumes memory only when instantiated, beneficial for specific use cases that might not be needed by all users.

  • Decoupling: Network requests are independent of views and controllers, continuing even if the latter are destroyed.

  • Centralized logging: Network logging is streamlined and centralized.

  • Reusable error handling: Common failure events like alerts can be reused for all requests.

  • Reusability across projects: The singleton’s main structure can be adapted for other projects with minor modifications to top-level static properties.

However, there are also reasons to be cautious about singletons:

  • Overuse: They can be misused to handle multiple responsibilities within a single class, leading to poorly designed and confusing code. Instead, create multiple singletons with distinct responsibilities.

  • Limited inheritance: Singletons cannot be subclassed.

  • Hidden dependencies: Excessive reliance on singletons can obscure dependencies, reducing modularity. Removing a singleton might expose missing imports in other classes, leading to issues, especially with external libraries.

  • Unexpected modifications: A class might alter shared properties within singletons during lengthy operations, leading to unpredictable outcomes in other classes without careful consideration.

  • Memory leaks: Leaks in a singleton can become problematic as the singleton itself is never deallocated.

However, these drawbacks can be mitigated with iOS app architecture best practices, such as:

  • Single responsibility: Each singleton should handle one specific responsibility.

  • Avoid storing rapidly changing data: Don’t use singletons for data frequently modified by multiple classes or threads if high accuracy is crucial.

  • Enable/disable based on dependencies: Design singletons to enable or disable features based on available dependencies.

  • Limit stored data: Avoid storing large amounts of data in singleton properties, as they persist throughout the application’s lifecycle unless managed manually.

Simple Singleton Example with AFNetworking

As a prerequisite, add AFNetworking to your project. The easiest way is through Cocoapods, with instructions found on its GitHub page.

While you’re at it, consider adding UIAlertController+Blocks and MBProgressHUD (also easily added via CocoaPods). These are optional but simplify progress indicators and alerts if you wish to implement them within the singleton on the AppDelegate window.

Once AFNetworking is integrated, create a new Cocoa Touch Class named NetworkManager as a subclass of NSObject. Add a class method to access the manager. Your NetworkManager.h file should resemble:

1
2
3
4
5
6
7
8
#import <Foundation/Foundation.h>
#import “AFNetworking.h”

@interface NetworkManager : NSObject

+ (id)sharedManager;

@end

Next, implement the basic initialization methods for the singleton and import the AFNetworking header. Your class implementation should look like this (assuming Automatic Reference Counting):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#import "NetworkManager.h"

@interface NetworkManager()

@end

@implementation NetworkManager

#pragma mark -
#pragma mark Constructors

static NetworkManager *sharedManager = nil;

+ (NetworkManager*)sharedManager {
    static dispatch_once_t once;
    dispatch_once(&once, ^
    {
        sharedManager = [[NetworkManager alloc] init];
    });
    return sharedManager;
}

- (id)init {
    if ((self = [super init])) {
    }
    return self;
}

@end

Now you’re ready to add properties and methods. As a quick test of singleton access, add the following to NetworkManager.h:

1
2
3
@property NSString *appID;

- (void)test;

And this to NetworkManager.m:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#define HOST @”http://www.apitesting.dev/”
static const in port = 80;


@implementation NetworkManager


//Set an initial property to init:

- (id)init {
    if ((self = [super init])) {
	self.appID = @1;
    }
    return self;
}

- (void)test {
	NSLog(@Testing out the networking singleton for appID: %@, HOST: %@, and PORT: %d, self.appID, HOST, port);
}

In your main ViewController.m file (or equivalent), import NetworkManager.h and add the following within viewDidLoad:

1
[[NetworkManager sharedManager] test];

Run the app, and you should see the following output:

Testing our the networking singleton for appID: 1, HOST: http://www.apitesting.dev/, and PORT: 80

While it’s unusual to mix #define, static const, and @property simultaneously, this demonstrates the options available. While “static const” is generally better for type safety, #define can be useful in string building with macros. Here, #define is used for brevity. Unless dealing with pointers, there’s little practical difference between these declaration methods.

With an understanding of #defines, constants, properties, and methods, let’s remove these test elements and proceed to more practical examples.

Networking Example

Consider an app requiring user login for access. Upon launch, it should check for a saved authentication token and, if present, make a GET request to the API to verify its validity.

In AppDelegate.m, register a default value for the token:

1
2
3
4
+ (void)initialize {
    NSDictionary *defaults = [NSDictionary dictionaryWithObjectsAndKeys:@"", @"token", nil];
    [[NSUserDefaults standardUserDefaults] registerDefaults:defaults];
}

Add a token check to NetworkManager with feedback via completion blocks. Design these blocks according to your needs. This example uses success with the response object data and failure with the error response string and status code. Note that the failure block is optional if the receiving side doesn’t require it.

NetworkManager.h

Above @interface:

1
2
typedef void (^NetworkManagerSuccess)(id responseObject);
typedef void (^NetworkManagerFailure)(NSString *failureReason, NSInteger statusCode);

Within @interface:

@property (nonatomic, strong) AFHTTPSessionManager *networkingManager;

1
- (void)tokenCheckWithSuccess:(NetworkManagerSuccess)success failure:(NetworkManagerFailure)failure;

NetworkManager.m:

Define the BASE_URL:

1
2
3
4
5
#define ENABLE_SSL 1
#define HOST @"http://www.apitesting.dev/"
#define PROTOCOL (ENABLE_SSL ? @"https://" : @"http://")
#define PORT @"80"
#define BASE_URL [NSString stringWithFormat:@"%@%@:%@", PROTOCOL, HOST, PORT]

Add helper methods for simplified authenticated requests and error parsing (assuming JSON web tokens):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
- (AFHTTPSessionManager*)getNetworkingManagerWithToken:(NSString*)token {
    if (self.networkingManager == nil) {
        self.networkingManager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:BASE_URL]];
        if (token != nil && [token length] > 0) {
            NSString *headerToken = [NSString stringWithFormat:@"%@ %@", @"JWT", token];
            [self.networkingManager.requestSerializer setValue:headerToken forHTTPHeaderField:@"Authorization"];
            // Example - [networkingManager.requestSerializer setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
        }
        self.networkingManager.requestSerializer = [AFJSONRequestSerializer serializer];
        self.networkingManager.responseSerializer.acceptableContentTypes = [self.networkingManager.responseSerializer.acceptableContentTypes setByAddingObjectsFromArray:@[@"text/html", @"application/json", @"text/json"]];
        self.networkingManager.securityPolicy = [self getSecurityPolicy];
    }
    return self.networkingManager;
}

- (id)getSecurityPolicy {
    return [AFSecurityPolicy defaultPolicy];
    /* Example - AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone];
    [policy setAllowInvalidCertificates:YES];
    [policy setValidatesDomainName:NO];
    return policy; */
}

- (NSString*)getError:(NSError*)error {
    if (error != nil) {
        NSData *errorData = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey];
        NSDictionary *responseObject = [NSJSONSerialization JSONObjectWithData: errorData options:kNilOptions error:nil];
        if (responseObject != nil && [responseObject isKindOfClass:[NSDictionary class]] && [responseObject objectForKey:@"message"] != nil && [[responseObject objectForKey:@"message"] length] > 0) {
            return [responseObject objectForKey:@"message"];
        }
    }
    return @"Server Error. Please try again later";
}

If you added MBProgressHUD, it can be used here:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#import "MBProgressHUD.h"

@interface NetworkManager()

@property (nonatomic, strong) MBProgressHUD *progressHUD;

@end



- (void)showProgressHUD {
    [self hideProgressHUD];
    self.progressHUD = [MBProgressHUD showHUDAddedTo:[[UIApplication sharedApplication] delegate].window animated:YES];
    [self.progressHUD removeFromSuperViewOnHide];
    self.progressHUD.bezelView.color = [UIColor colorWithWhite:0.0 alpha:1.0];
    self.progressHUD.contentColor = [UIColor whiteColor];
}

- (void)hideProgressHUD {
    if (self.progressHUD != nil) {
        [self.progressHUD hideAnimated:YES];
        [self.progressHUD removeFromSuperview];
        self.progressHUD = nil;
    }
}

And the token check request:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)tokenCheckWithSuccess:(NetworkManagerSuccess)success failure:(NetworkManagerFailure)failure {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSString *token = [defaults objectForKey:@"token"];
    if (token == nil || [token length] == 0) {
        if (failure != nil) {
            failure(@"Invalid Token", -1);
        }
        return;
    }
    [self showProgressHUD];
    NSMutableDictionary *params = [NSMutableDictionary dictionary];
    [[self getNetworkingManagerWithToken:token] GET:@"/checktoken" parameters:params progress:nil success:^(NSURLSessionTask *task, id responseObject) {
        [self hideProgressHUD];
        if (success != nil) {
            success(responseObject);
        }
    } failure:^(NSURLSessionTask *operation, NSError *error) {
        [self hideProgressHUD];
        NSString *errorMessage = [self getError:error];
        if (failure != nil) {
            failure(errorMessage, ((NSHTTPURLResponse*)operation.response).statusCode);
        }
    }];
}

Now, in ViewController.m’s viewWillAppear method, call this singleton method. Observe the request’s simplicity and minimal implementation on the View Controller side:

1
2
3
4
5
6
7
    [[NetworkManager sharedManager] tokenCheckWithSuccess:^(id responseObject) {
        // Allow User Access and load content
        //[self loadContent];
    } failure:^(NSString *failureReason, NSInteger statusCode) {
        // Logout user if logged in and deny access and show login view
        //[self showLoginView];
    }];

Notice how this code snippet can be reused in virtually any app that needs to verify authentication on launch.

Similarly, you can handle a POST request for login:

NetworkManager.h:

1
- (void)authenticateWithEmail:(NSString*)email password:(NSString*)password success:(NetworkManagerSuccess)success failure:(NetworkManagerFailure)failure;

NetworkManager.m:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)authenticateWithEmail:(NSString*)email password:(NSString*)password success:(NetworkManagerSuccess)success failure:(NetworkManagerFailure)failure {
    if (email != nil && [email length] > 0 && password != nil && [password length] > 0) {
        [self showProgressHUD];
        NSMutableDictionary *params = [NSMutableDictionary dictionary];
        [params setObject:email forKey:@"email"];
        [params setObject:password forKey:@"password"];
        [[self getNetworkingManagerWithToken:nil] POST:@"/authenticate" parameters:params progress:nil success:^(NSURLSessionTask *task, id responseObject) {
            [self hideProgressHUD];
            if (success != nil) {
                success(responseObject);
            }
        } failure:^(NSURLSessionTask *operation, NSError *error) {
            [self hideProgressHUD];
            NSString *errorMessage = [self getError:error];
            if (failure != nil) {
                failure(errorMessage, ((NSHTTPURLResponse*)operation.response).statusCode);
            }
        }];
    } else {
        if (failure != nil) {
            failure(@"Email and Password Required", -1);
        }
    }
}

You could add alerts using AlertController+Blocks on the AppDelegate window or send failure objects back to the view controller. Additionally, you could save user credentials here or let the view controller handle it. A separate UserManager singleton for managing credentials and permissions, communicating directly with NetworkManager, might be preferable.

Again, the view controller implementation remains simple:

1
2
3
4
5
6
7
8
9
- (void)loginUser {
    NSString *email = @"test@apitesting.dev";
    NSString *password = @"SomeSillyEasyPassword555";
    [[NetworkManager sharedManager] authenticateWithEmail:email password:password success:^(id responseObject) {
        // Save User Credentials and show content
    } failure:^(NSString *failureReason, NSInteger statusCode) {
        // Explain to user why authentication failed
    }];
}

However, the API version and device type are missing. Also, the endpoint has changed from “/checktoken” to “/token”. Thankfully, due to centralized networking, updates are straightforward.

Create a helper method for these parameters:

1
2
3
4
5
6
7
8
9
#define API_VERSION @"1.0"
#define DEVICE_TYPE UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad ? @"tablet" : @"phone"

- (NSMutableDictionary*)getBaseParams {
    NSMutableDictionary *baseParams = [NSMutableDictionary dictionary];
    [baseParams setObject:@"version" forKey:API_VERSION];
    [baseParams setObject:@"device_type" forKey:DEVICE_TYPE];
    return baseParams;
}

You can easily add more common parameters in the future. Now update the token check and authentication methods:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10

    NSMutableDictionary *params = [self getBaseParams];
    [[self getNetworkingManagerWithToken:token] GET:@"/checktoken" parameters:params progress:nil success:^(NSURLSessionTask *task, id responseObject) {



        NSMutableDictionary *params = [self getBaseParams];
        [params setObject:email forKey:@"email"];
        [params setObject:password forKey:@"password"];
        [[self getNetworkingManagerWithToken:nil] POST:@"/authenticate" parameters:params progress:nil success:^(NSURLSessionTask *task, id responseObject) {

Conclusion

This tutorial showcased centralized networking parameters and methods within a singleton manager, significantly simplifying view controller implementations. Future updates are now simpler, faster, and most importantly, the networking logic is decoupled from the user experience.

While this example focused on a networking singleton, the same principles apply to other centralized functions like:

  • Managing user state and permissions
  • Routing touch actions for app navigation
  • Handling video and audio
  • Implementing analytics
  • Managing notifications
  • Interacting with peripherals
  • And much more…

Although centered on iOS app architecture, these concepts extend to Android and even JavaScript. Moreover, this well-defined and function-oriented code facilitates porting apps to new platforms.

In summary, investing time in early project planning to establish key singleton methods, like the networking example above, results in cleaner, simpler, and more maintainable code in the long run.

Licensed under CC BY-NC-SA 4.0