Best architectural approaches for building iOS networking applications (REST clients)

后端 未结 13 1211
-上瘾入骨i
-上瘾入骨i 2020-12-22 14:11

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\

13条回答
  •  余生分开走
    2020-12-22 15:07

    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.

提交回复
热议问题