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'm still confused. What is the best architecture for an iOS networked application? I mean basic abstract framework, patterns, which will fit every networking application whether it is a small app which only have a few server requests or a complex REST client. Apple recommends to use MVC as a basic architectural approach for all iOS applications, but neither MVC nor the more modern MVVM patterns explain where to put network logic code and how to organize it in general. Do I need to develop something like MVCS(S for Service) and in this Service layer put all API requests and other networking logic, which in perspective may be really complex? After doing some research I found two basic approaches for this. Here it was recommended to create a separate class for every network request to web-service API (like LoginRequest class or PostCommentRequest class and so on) which all inherits from the base request abstract class AbstractBaseRequest and in addition to create some global network manager which encapsulates common networking code and other preferences (it may be AFNetworking customisation or RestKit tuning, if the we have complex object mappings and persistence, or even an own network communication implementation with standard API). But this approach seems an overhead for me. Another approach is to have some singleton API dispatcher or manager class as in the first approach, but not to create classes for every request and instead to encapsulate every request as an instance public method of this manager class like: fetchContacts, loginUser methods, etc. So, what is the best and correct way? Are there other interesting approaches I don't know yet? And should I create another layer for all this networking stuff like Service, or NetworkProvider layer or whatever on top of my MVC architecture, or this layer should be integrated (injected) into existing MVC layers e.g. Model? I know there exists beautiful approaches, or how then such mobile monsters like Facebook client or LinkedIn client deal with exponentially growing complexity of networking logic? I know there are no exact and formal answer to the problem. The goal of this question is to collect the most interesting approaches from experienced iOS developers. The best suggested approach will be marked as accepted and awarded with a reputation bounty, others will be upvoted. It is mostly a theoretical and research question. I want to understand basic, abstract and correct architectural approach for networking applications in iOS. I hope for detailed explanation from experienced developers.


当前回答

根据这个问题的目的,我想描述一下我们的架构方法。

体系结构方法

我们通用的iOS应用的架构基于以下模式:服务层、MVVM、UI数据绑定、依赖注入;函数式响应式编程范式。

我们可以将一个典型的面向消费者的应用程序划分为以下逻辑层:

组装 模型 服务 存储 经理 协调员 用户界面 基础设施

组装层是应用程序的引导点。它包含一个依赖注入容器和应用程序对象及其依赖项的声明。这一层还可能包含应用程序的配置(url,第三方服务密钥等)。为此,我们使用了Typhoon库。

模型层包含域模型类、验证、映射。我们使用Mantle库来映射我们的模型:它支持序列化/反序列化到JSON格式和NSManagedObject模型。对于模型的验证和表单表示,我们使用FXForms和FXModelValidation库。

服务层声明了用于与外部系统交互的服务,以便发送或接收在域模型中表示的数据。因此,通常我们有用于与服务器api(每个实体)通信的服务、消息传递服务(如PubNub)、存储服务(如Amazon S3)等。基本上,服务包装SDK(例如PubNub SDK)提供的对象或实现它们自己的通信逻辑。对于一般的网络,我们使用AFNetworking库。

存储层的目的是组织设备上的本地数据存储。为此我们使用Core Data或Realm(两者都有优缺点,使用哪个取决于具体的规格)。对于CoreData设置,我们使用MDMCoreData库和一堆类-存储-(类似于服务),它们为每个实体提供对本地存储的访问。对于Realm,我们只是使用类似的存储来访问本地存储。

管理层是我们的抽象/包装器所在的地方。

经理角色可以是:

凭据管理器及其不同的实现(keychain, NSDefaults,…) 当前会话管理器,知道如何保持和提供当前用户会话 捕获管道,提供对媒体设备的访问(视频录制,音频,拍照) BLE管理器,提供对蓝牙服务和外围设备的访问 地理位置管理器 ...

因此,管理器的角色可以是任何实现应用程序工作所需的特定方面或关注点的逻辑的对象。

我们尽量避免singleton,但是如果需要的话,这一层是他们居住的地方。

协调器层提供依赖于其他层(服务、存储、模型)对象的对象,以便将它们的逻辑组合成特定模块(功能、屏幕、用户故事或用户体验)所需的一个工作序列。它通常链接异步操作,并知道如何对它们的成功和失败情况作出反应。作为一个例子,您可以想象一个消息传递特性和对应的MessagingCoordinator对象。处理发送消息操作可能是这样的:

验证消息(模型层) 本地保存消息(消息存储) 上传消息附件(amazon s3服务) 更新消息状态和附件url并在本地保存消息(消息存储) 将消息序列化为JSON格式(模型层) 向PubNub发布消息(PubNub服务) 更新消息状态和属性并将其保存在本地(消息存储)

在上述每一个步骤中,都会相应地处理一个错误。

UI层由以下子层组成:

视图模型 视图控制器 视图

为了避免大量的视图控制器,我们使用MVVM模式,并在视图模型中实现UI表示所需的逻辑。ViewModel通常有协调器和管理器作为依赖项。viewcontroller和某些类型的视图(例如表视图单元格)使用的viewmodel。视图控制器和视图模型之间的粘合剂是数据绑定和命令模式。为了使它有可能有胶水,我们使用ReactiveCocoa库。

我们还使用ReactiveCocoa及其RACSignal概念作为所有协调器、服务和存储方法的接口和返回值类型。这允许我们链式操作,并行或串行运行它们,以及ReactiveCocoa提供的许多其他有用的东西。

我们尝试以声明式的方式实现UI行为。数据绑定和自动布局有助于实现这一目标。

基础设施层包含应用程序工作所需的所有帮助程序、扩展和实用程序。


这种方法对我们和我们通常构建的那些类型的应用都很有效。但你应该明白,这只是一种主观的方法,应该根据具体的团队目的进行调整/改变。

希望这对你有所帮助!

你也可以在这篇文章中找到更多关于iOS开发过程的信息

其他回答

To my mind all software architecture is driven by need. If this is for learning or personal purposes, then decide the primary goal and have that drive the architecture. If this is a work for hire, then the business need is paramount. The trick is to not let shiny things distract you from the real needs. I find this hard to do. There are always new shiny things appearing in this business and lots of them are not useful, but you can't always tell that up front. Focus on the need and be willing to abandon bad choices if you can.

For example, I recently did a quick prototype of a photo sharing app for a local business. Since the business need was to do something quick and dirty, the architecture ended up being some iOS code to pop up a camera and some network code attached to a Send Button that uploaded the image to a S3 store and wrote to a SimpleDB domain. The code was trivial and the cost minimal and the client has an scalable photo collection accessible over the web with REST calls. Cheap and dumb, the app had lots of flaws and would lock the UI on occasion, but it would be a waste to do more for a prototype and it allows them to deploy to their staff and generate thousands of test images easily without performance or scalability concerns. Crappy architecture, but it fit the need and cost perfectly.

Another project involved implementing a local secure database which synchronizes with the company system in the background when the network is available. I created a background synchronizer that used RestKit as it seemed to have everything I needed. But I had to write so much custom code for RestKit to deal with idiosyncratic JSON that I could have done it all quicker by writing my own JSON to CoreData transformations. However, the customer wanted to bring this app in house and I felt that RestKit would be similar to the frameworks that they used on other platforms. I waiting to see if that was a good decision.

Again, the issue to me is to focus on the need and let that determine the architecture. I try like hell to avoid using third party packages as they bring costs that only appears after the app has been in the field for a while. I try to avoid making class hierarchies as they rarely pay off. If I can write something in a reasonable period of time instead of adopting a package that doesn't fit perfectly, then I do it. My code is well structured for debugging and appropriately commented, but third party packages rarely are. With that said, I find AF Networking too useful to ignore and well structured, well commented, and maintained and I use it a lot! RestKit covers a lot of common cases, but I feel like I've been in a fight when I use it, and most of the data sources I encounter are full of quirks and issues that are best handled with custom code. In my last few apps I just use the built in JSON converters and write a few utility methods.

One pattern I always use is to get the network calls off the main thread. The last 4-5 apps I've done set up a background timer task using dispatch_source_create that wakes up every so often and does network tasks as needed. You need to do some thread safety work and make sure that UI modifying code gets sent to the main thread. It also helps to do your onboarding/initialization in such a way that the user doesn't feel burdened or delayed. So far this has been working rather well. I suggest looking into these things.

Finally, I think that as we work more and as the OS evolves, we tend to develop better solutions. It has taken me years to get over my belief that I have to follow patterns and designs that other people claim are mandatory. If I am working in a context where that is part of the local religion, ahem, I mean the departmental best engineering practices, then I follow the customs to the letter, that's what they are paying me for. But I rarely find that following older designs and patterns is the optimal solution. I always try to look at the solution through the prism of the business needs and build the architecture to match it and keep things as simple as they can be. When I feel like there isn't enough there, but everything works correctly, then I'm on the right track.

我们根据具体情况使用几种方法。对于大多数事情来说,AFNetworking是最简单和最健壮的方法,因为你可以设置标题,上传多部分数据,使用GET, POST, PUT和DELETE,还有一堆附加的UIKit类别,允许你从url设置图像。在一个有很多调用的复杂应用程序中,我们有时会将其抽象为我们自己的方便方法,就像这样:

-(void)makeRequestToUrl:(NSURL *)url withParameters:(NSDictionary *)parameters success:(void (^)(id responseObject))success failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure;

有一些情况下,AFNetworking是不合适的,但是,如你正在创建一个框架或其他库组件,因为AFNetworking可能已经在另一个代码库。在这种情况下,你可以使用NSMutableURLRequest内联,如果你正在做一个单独的调用或抽象到一个请求/响应类。

从纯类设计的角度来看,你通常会有这样的东西:

Your view controllers controlling one or more views 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.

在我的情况下,我通常使用ResKit库来设置网络层。它提供了易于使用的解析。它减少了我为不同的响应设置映射的工作量。

我只添加了一些代码来自动设置映射。 我为我的模型定义了基类(不是协议,因为有很多代码来检查一些方法是否被实现,模型本身的代码更少):

MappableEntry。h

@interface MappableEntity : NSObject

+ (NSArray*)pathPatterns;
+ (NSArray*)keyPathes;
+ (NSArray*)fieldsArrayForMapping;
+ (NSDictionary*)fieldsDictionaryForMapping;
+ (NSArray*)relationships;

@end

MappableEntry米。

@implementation MappableEntity

+(NSArray*)pathPatterns {
    return @[];
}

+(NSArray*)keyPathes {
    return nil;
}

+(NSArray*)fieldsArrayForMapping {
    return @[];
}

+(NSDictionary*)fieldsDictionaryForMapping {
    return @{};
}

+(NSArray*)relationships {
    return @[];
}

@end

关系是表示响应中嵌套对象的对象:

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

然后我像这样为RestKit设置映射:

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

MappableEntry实现的一些例子:

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

现在关于请求包装:

我有头文件与块定义,以减少行长度在所有APIRequest类:

APICallbacks.h

typedef void(^SuccessCallback)();
typedef void(^SuccessCallbackWithObjects)(NSArray *objects);
typedef void(^ErrorCallback)(NSError *error);
typedef void(^ProgressBlock)(float progress);

我的APIRequest类的例子,我正在使用:

LoginAPI。h

@interface LoginAPI : NSObject

- (void)loginWithCredentials:(Credentials*)credentials onSuccess:(SuccessCallbackWithObjects)onSuccess onError:(ErrorCallback)onError;

@end

公元LoginAPI。

@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

在代码中,你只需要初始化API对象,并在需要时调用它:

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

我的代码并不完美,但很容易设置一次,并用于不同的项目。如果任何人都感兴趣,我可以花一些时间,在GitHub和CocoaPods上为它做一个通用的解决方案。

在设计应用程序时,我避免使用单例。他们是很多人的典型选择,但我认为你可以在其他地方找到更优雅的解决方案。通常我做的是在CoreData中构建我的实体,然后把我的REST代码放在NSManagedObject类别中。例如,如果我想创建并POST一个新用户,我会这样做:

User* newUser = [User createInManagedObjectContext:managedObjectContext];
[newUser postOnSuccess:^(...) { ... } onFailure:^(...) { ... }];

我使用RESTKit进行对象映射,并在启动时初始化它。我发现通过单例路由您的所有调用是浪费时间,并添加了许多不需要的样板文件。

在NSManagedObject + Extensions.m:

+ (instancetype)createInContext:(NSManagedObjectContext*)context
{
    NSAssert(context.persistentStoreCoordinator.managedObjectModel.entitiesByName[[self entityName]] != nil, @"Entity with name %@ not found in model. Is your class name the same as your entity name?", [self entityName]);
    return [NSEntityDescription insertNewObjectForEntityForName:[self entityName] inManagedObjectContext:context];
}

在NSManagedObject + Networking.m:

- (void)getOnSuccess:(RESTSuccess)onSuccess onFailure:(RESTFailure)onFailure blockInput:(BOOL)blockInput
{
    [[RKObjectManager sharedManager] getObject:self path:nil parameters:nil success:onSuccess failure:onFailure];
    [self handleInputBlocking:blockInput];
}

当可以通过类别扩展公共基类的功能时,为什么还要添加额外的helper类呢?

如果你对我的解决方案更详细的信息感兴趣,请告诉我。我很乐意分享。