I\'m an iOS developer with some experience and this question is really interesting to me. I saw a lot of different resources and materials on this topic, but nevertheless I\
This question has a lot of excellent and extensive answers already, but I feel I have to mention it since no one else has.
Alamofire for Swift. https://github.com/Alamofire/Alamofire
It's created by the same people as AFNetworking, but is more directly designed with Swift in mind.
According to the goal of this question, I'd like to describe our architecture approach.
Our general iOS application’s architecture stands on following patterns: Service layers, MVVM, UI Data Binding, Dependency Injection; and Functional Reactive Programming paradigm.
We can slice a typical consumer facing application into following logical layers:
Assembly layer is a bootstrap point of our application. It contains a Dependency Injection container and declarations of application’s objects and their dependencies. This layer also might contain application’s configuration (urls, 3rd party services keys and so on). For this purpose we use Typhoon library.
Model layer contains domain models classes, validations, mappings. We use Mantle library for mapping our models: it supports serialization/deserialization into JSON
format and NSManagedObject
models. For validation and form representation of our models we use FXForms and FXModelValidation libraries.
Services layer declares services which we use for interacting with external systems in order to send or receive data which is represented in our domain model. So usually we have services for communication with server APIs (per entity), messaging services (like PubNub), storage services (like Amazon S3), etc. Basically services wrap objects provided by SDKs (for example PubNub SDK) or implement their own communication logic. For general networking we use AFNetworking library.
Storage layer’s purpose is to organize local data storage on the device. We use Core Data or Realm for this (both have pros and cons, decision of what to use is based on concrete specs). For Core Data setup we use MDMCoreData library and bunch of classes - storages - (similar to services) which provide access to local storage for every entity. For Realm we just use similar storages to have access to local storage.
Managers layer is a place where our abstractions/wrappers live.
In a manager role could be:
So, in role of manager could be any object which implements logic of a particular aspect or concern needed for application working.
We try to avoid Singletons, but this layer is a place where they live if they are needed.
Coordinators layer provides objects which depends on objects from other layers (Service, Storage, Model) in order to combine their logic into one sequence of work needed for certain module (feature, screen, user story or user experience). It usually chains asynchronous operations and knows how to react on their success and failure cases. As an example you can imagine a messaging feature and corresponding MessagingCoordinator
object. Handling sending message operation might look like this:
On each of above steps an error is handled correspondingly.
UI layer consists of following sublayers:
In order to avoid Massive View Controllers we use MVVM pattern and implement logic needed for UI presentation in ViewModels. A ViewModel usually has coordinators and managers as dependencies. ViewModels used by ViewControllers and some kinds of Views (e.g. table view cells). The glue between ViewControllers and ViewModels is Data Binding and Command pattern. In order to make it possible to have that glue we use ReactiveCocoa library.
We also use ReactiveCocoa and its RACSignal
concept as an interface and returning value type of all coordinators, services, storages methods. This allows us to chain operations, run them parallelly or serially, and many other useful things provided by ReactiveCocoa.
We try to implement our UI behavior in declarative way. Data Binding and Auto Layout helps a lot to achieve this goal.
Infrastructure layer contains all the helpers, extensions, utilities needed for application work.
This approach works well for us and those types of apps we usually build. But you should understand, that this is just a subjective approach that should be adapted/changed for concrete team's purpose.
Hope this will help you!
Also you can find more information about iOS development process in this blog post iOS Development as a Service
We use a few approaches depending on the situation. For most things AFNetworking is the simplest and most robust approach in that you can set headers, upload multipart data, use GET, POST, PUT & DELETE and there are a bunch of additional categories for UIKit which allow you to for example set an image from a url. In a complex app with a lot of calls we sometimes abstract this down to a convenience method of our own which would be something like:
-(void)makeRequestToUrl:(NSURL *)url withParameters:(NSDictionary *)parameters success:(void (^)(id responseObject))success failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure;
There are a few situations where AFNetworking isn't appropriate however such as where you are creating a framework or other library component as AFNetworking may already be in another code base. In this situation you would use an NSMutableURLRequest either inline if you are making a single call or abstracted into a request / response class.
From a purely class design perspective, you will usually have something like this:
Data model class - It really depends upon how many real distinct entities you are dealing with, and how they are related.
For example, if you have an array of items to be displayed in four different representations (list, chart, graph etc), you will have one data model class for list of items, one more for an item. The list of item class will be shared by four view controllers - all children of a tab bar controller or a nav controller.
Data model classes will come handy in not only displaying data, but also serializing them wherein each of them can expose their own serialization format through JSON / XML / CSV (or anything else) export methods.
It is important to understand that you also need API request builder classes that map directly with your REST API endpoints. Let's say you have an API that logs the user in - so your Login API builder class will create POST JSON payload for login api. In another example, an API request builder class for list of catalog items API will create GET query string for corresponding api and fire the REST GET query.
These API request builder classes will usually receive data from view controllers and also pass the same data back to view controllers for UI update / other operations. View controllers will then decide how to update Data Model objects with that data.
Finally, the heart of the REST client - API data fetcher class which is oblivious to all sorts of API requests your app makes. This class will more likely be a singleton, but as others pointed out, it doesn't have to be a singleton.
Note that the link is just a typical implementation and does not take into consideration scenarios like session, cookies etc, but it is enough to get you going without using any 3rd party frameworks.
In my situation I'm usually using ResKit library to set up the network layer. It provides easy-to-use parsing. It reduces my effort on setting up the mapping for different responses and stuff.
I only add some code to setup the mapping automatically. I define base class for my models (not protocol because of lot of code to check if some method is implemented or not, and less code in models itself):
MappableEntry.h
@interface MappableEntity : NSObject
+ (NSArray*)pathPatterns;
+ (NSArray*)keyPathes;
+ (NSArray*)fieldsArrayForMapping;
+ (NSDictionary*)fieldsDictionaryForMapping;
+ (NSArray*)relationships;
@end
MappableEntry.m
@implementation MappableEntity
+(NSArray*)pathPatterns {
return @[];
}
+(NSArray*)keyPathes {
return nil;
}
+(NSArray*)fieldsArrayForMapping {
return @[];
}
+(NSDictionary*)fieldsDictionaryForMapping {
return @{};
}
+(NSArray*)relationships {
return @[];
}
@end
Relationships are objects which represent nested objects in response:
RelationshipObject.h
@interface RelationshipObject : NSObject
@property (nonatomic,copy) NSString* source;
@property (nonatomic,copy) NSString* destination;
@property (nonatomic) Class mappingClass;
+(RelationshipObject*)relationshipWithKey:(NSString*)key andMappingClass:(Class)mappingClass;
+(RelationshipObject*)relationshipWithSource:(NSString*)source destination:(NSString*)destination andMappingClass:(Class)mappingClass;
@end
RelationshipObject.m
@implementation RelationshipObject
+(RelationshipObject*)relationshipWithKey:(NSString*)key andMappingClass:(Class)mappingClass {
RelationshipObject* object = [[RelationshipObject alloc] init];
object.source = key;
object.destination = key;
object.mappingClass = mappingClass;
return object;
}
+(RelationshipObject*)relationshipWithSource:(NSString*)source destination:(NSString*)destination andMappingClass:(Class)mappingClass {
RelationshipObject* object = [[RelationshipObject alloc] init];
object.source = source;
object.destination = destination;
object.mappingClass = mappingClass;
return object;
}
@end
Then I'm setting up the mapping for RestKit like this:
ObjectMappingInitializer.h
@interface ObjectMappingInitializer : NSObject
+(void)initializeRKObjectManagerMapping:(RKObjectManager*)objectManager;
@end
ObjectMappingInitializer.m
@interface ObjectMappingInitializer (Private)
+ (NSArray*)mappableClasses;
@end
@implementation ObjectMappingInitializer
+(void)initializeRKObjectManagerMapping:(RKObjectManager*)objectManager {
NSMutableDictionary *mappingObjects = [NSMutableDictionary dictionary];
// Creating mappings for classes
for (Class mappableClass in [self mappableClasses]) {
RKObjectMapping *newMapping = [RKObjectMapping mappingForClass:mappableClass];
[newMapping addAttributeMappingsFromArray:[mappableClass fieldsArrayForMapping]];
[newMapping addAttributeMappingsFromDictionary:[mappableClass fieldsDictionaryForMapping]];
[mappingObjects setObject:newMapping forKey:[mappableClass description]];
}
// Creating relations for mappings
for (Class mappableClass in [self mappableClasses]) {
RKObjectMapping *mapping = [mappingObjects objectForKey:[mappableClass description]];
for (RelationshipObject *relation in [mappableClass relationships]) {
[mapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:relation.source toKeyPath:relation.destination withMapping:[mappingObjects objectForKey:[relation.mappingClass description]]]];
}
}
// Creating response descriptors with mappings
for (Class mappableClass in [self mappableClasses]) {
for (NSString* pathPattern in [mappableClass pathPatterns]) {
if ([mappableClass keyPathes]) {
for (NSString* keyPath in [mappableClass keyPathes]) {
[objectManager addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:[mappingObjects objectForKey:[mappableClass description]] method:RKRequestMethodAny pathPattern:pathPattern keyPath:keyPath statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]];
}
} else {
[objectManager addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:[mappingObjects objectForKey:[mappableClass description]] method:RKRequestMethodAny pathPattern:pathPattern keyPath:nil statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]];
}
}
}
// Error Mapping
RKObjectMapping *errorMapping = [RKObjectMapping mappingForClass:[Error class]];
[errorMapping addAttributeMappingsFromArray:[Error fieldsArrayForMapping]];
for (NSString *pathPattern in Error.pathPatterns) {
[[RKObjectManager sharedManager] addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:errorMapping method:RKRequestMethodAny pathPattern:pathPattern keyPath:nil statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassClientError)]];
}
}
@end
@implementation ObjectMappingInitializer (Private)
+ (NSArray*)mappableClasses {
return @[
[FruiosPaginationResults class],
[FruioItem class],
[Pagination class],
[ContactInfo class],
[Credentials class],
[User class]
];
}
@end
Some example of MappableEntry implementation:
User.h
@interface User : MappableEntity
@property (nonatomic) long userId;
@property (nonatomic, copy) NSString *username;
@property (nonatomic, copy) NSString *email;
@property (nonatomic, copy) NSString *password;
@property (nonatomic, copy) NSString *token;
- (instancetype)initWithUsername:(NSString*)username email:(NSString*)email password:(NSString*)password;
- (NSDictionary*)registrationData;
@end
User.m
@implementation User
- (instancetype)initWithUsername:(NSString*)username email:(NSString*)email password:(NSString*)password {
if (self = [super init]) {
self.username = username;
self.email = email;
self.password = password;
}
return self;
}
- (NSDictionary*)registrationData {
return @{
@"username": self.username,
@"email": self.email,
@"password": self.password
};
}
+ (NSArray*)pathPatterns {
return @[
[NSString stringWithFormat:@"/api/%@/users/register", APIVersionString],
[NSString stringWithFormat:@"/api/%@/users/login", APIVersionString]
];
}
+ (NSArray*)fieldsArrayForMapping {
return @[ @"username", @"email", @"password", @"token" ];
}
+ (NSDictionary*)fieldsDictionaryForMapping {
return @{ @"id": @"userId" };
}
@end
Now about the Requests wrapping:
I have header file with blocks definition, to reduce line length in all APIRequest classes:
APICallbacks.h
typedef void(^SuccessCallback)();
typedef void(^SuccessCallbackWithObjects)(NSArray *objects);
typedef void(^ErrorCallback)(NSError *error);
typedef void(^ProgressBlock)(float progress);
And Example of my APIRequest class that I'm using:
LoginAPI.h
@interface LoginAPI : NSObject
- (void)loginWithCredentials:(Credentials*)credentials onSuccess:(SuccessCallbackWithObjects)onSuccess onError:(ErrorCallback)onError;
@end
LoginAPI.m
@implementation LoginAPI
- (void)loginWithCredentials:(Credentials*)credentials onSuccess:(SuccessCallbackWithObjects)onSuccess onError:(ErrorCallback)onError {
[[RKObjectManager sharedManager] postObject:nil path:[NSString stringWithFormat:@"/api/%@/users/login", APIVersionString] parameters:[credentials credentialsData] success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
onSuccess(mappingResult.array);
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
onError(error);
}];
}
@end
And all you need to do in code, simply initialize API object and call it whenever you need it:
SomeViewController.m
@implementation SomeViewController {
LoginAPI *_loginAPI;
// ...
}
- (void)viewDidLoad {
[super viewDidLoad];
_loginAPI = [[LoginAPI alloc] init];
// ...
}
// ...
- (IBAction)signIn:(id)sender {
[_loginAPI loginWithCredentials:_credentials onSuccess:^(NSArray *objects) {
// Success Block
} onError:^(NSError *error) {
// Error Block
}];
}
// ...
@end
My code isn't perfect, but it's easy to set once and use for different projects. If it's interesting to anyone, mb I could spend some time and make a universal solution for it somewhere on GitHub and CocoaPods.
I think for now medium project use MVVM architecture and Big project use VIPER architecture and try to achieved
And Architectural approaches for building iOS networking applications (REST clients)
Separation concern for clean and readable code avoid duplication:
import Foundation
enum DataResponseError: Error {
case network
case decoding
var reason: String {
switch self {
case .network:
return "An error occurred while fetching data"
case .decoding:
return "An error occurred while decoding data"
}
}
}
extension HTTPURLResponse {
var hasSuccessStatusCode: Bool {
return 200...299 ~= statusCode
}
}
enum Result<T, U: Error> {
case success(T)
case failure(U)
}
dependancy inversion
protocol NHDataProvider {
func fetchRemote<Model: Codable>(_ val: Model.Type, url: URL, completion: @escaping (Result<Codable, DataResponseError>) -> Void)
}
Main responsible:
final class NHClientHTTPNetworking : NHDataProvider {
let session: URLSession
init(session: URLSession = URLSession.shared) {
self.session = session
}
func fetchRemote<Model: Codable>(_ val: Model.Type, url: URL,
completion: @escaping (Result<Codable, DataResponseError>) -> Void) {
let urlRequest = URLRequest(url: url)
session.dataTask(with: urlRequest, completionHandler: { data, response, error in
guard
let httpResponse = response as? HTTPURLResponse,
httpResponse.hasSuccessStatusCode,
let data = data
else {
completion(Result.failure(DataResponseError.network))
return
}
guard let decodedResponse = try? JSONDecoder().decode(Model.self, from: data) else {
completion(Result.failure(DataResponseError.decoding))
return
}
completion(Result.success(decodedResponse))
}).resume()
}
}
You will find here is the GitHub MVVM architecture with rest API Swift Project