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应用程序都是不同的,所以我认为这里有不同的方法可以考虑,但我通常是这样做的: 创建一个中央管理器(单例)类来处理所有API请求(通常命名为apiccommunicator),每个实例方法都是一个API调用。有一个中心(非公共)方法:

-(RACSignal *)sendGetToServerToSubPath:(NSString *)path withParameters:(NSDictionary *)params;

For the record, I use 2 major libraries/frameworks, ReactiveCocoa and AFNetworking. ReactiveCocoa handles async networking responses perfectly, you can do (sendNext:, sendError:, etc.). This method calls the API, gets the results and sends them through RAC in 'raw' format (like NSArray what AFNetworking returns). Then a method like getStuffList: which called the above method subscribes to it's signal, parses the raw data into objects (with something like Motis) and sends the objects one by one to the caller (getStuffList: and similar methods also return a signal that the controller can subscribe to). The subscribed controller receives the objects by subscribeNext:'s block and handles them. I tried many ways in different apps but this one worked the best out of all so I've been using this in a few apps recently, it fits both small and big projects and it's easy to extend and maintain if something needs to be modified. Hope this helps, I'd like to hear others' opinions about my approach and maybe how others think this could be maybe improved.

其他回答

我们根据具体情况使用几种方法。对于大多数事情来说,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.

我认为目前中型项目使用MVVM架构,大型项目使用VIPER架构 并努力实现

面向协议编程 软件设计模式 S.O.L.D原则 泛型编程 不要重复自己(DRY)

以及构建iOS网络应用程序的架构方法(REST客户端)

对于代码干净易读的分离问题,避免重复:

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)
}

依存关系反演

 protocol NHDataProvider {
        func fetchRemote<Model: Codable>(_ val: Model.Type, url: URL, completion: @escaping (Result<Codable, DataResponseError>) -> Void)
    }

主要负责:

  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()
        }
    }

你会发现这里是GitHub MVVM架构与rest API Swift项目

在移动软件工程中,应用最广泛的是Clean Architecture + MVVM和Redux模式。

Clean Architecture + MVVM由3层组成: 域、表示、数据层。 表示层和数据存储库层依赖于域层:

Presentation Layer -> Domain Layer <- Data Repositories Layer

表示层由视图模型和视图(MVVM)组成:

Presentation Layer (MVVM) = ViewModels + Views
Domain Layer = Entities + Use Cases + Repositories Interfaces
Data Repositories Layer = Repositories Implementations + API (Network) + Persistence DB

在本文中,将对Clean Architecture + MVVM进行更详细的描述 https://tech.olx.com/clean-architecture-and-mvvm-on-ios-c9d167d9f5b3

我想了解iOS中网络应用的基本、抽象和正确的架构方法

构建应用程序架构不存在“最好的”或“最正确的”方法。这是一个非常有创意的工作。您应该始终选择最直接和可扩展的体系结构,这对于任何开始开发您的项目的开发人员或您团队中的其他开发人员来说都是清楚的,但我同意,可以有“好”和“坏”的体系结构。

你说:

从经验丰富的iOS开发者那里收集最有趣的方法

I don't think that my approach is the most interesting or correct, but I've used it in several projects and satisfied with it. It is a hybrid approach of the ones you have mentioned above, and also with improvements from my own research efforts. I'm interesting in the problems of building approaches, which combine several well-known patterns and idioms. I think a lot of Fowler's enterprise patterns can be successfully applied to the mobile applications. Here is a list of the most interesting ones, which we can apply for creating an iOS application architecture (in my opinion): Service Layer, Unit Of Work, Remote Facade, Data Transfer Object, Gateway, Layer Supertype, Special Case, Domain Model. You should always correctly design a model layer and always don't forget about the persistence (it can significantly increase your app's performance). You can use Core Data for this. But you should not forget, that Core Data is not an ORM or a database, but an object graph manager with persistence as a good option of it. So, very often Core Data can be too heavy for your needs and you can look at new solutions such as Realm and Couchbase Lite, or build your own lightweight object mapping/persistence layer, based on raw SQLite or LevelDB. Also I advice you to familiarize yourself with the Domain Driven Design and CQRS.

首先,我认为,我们应该为网络创建另一个层,因为我们不想要肥胖的控制器或沉重的、不堪重负的模型。我不相信那些胖模特,瘦控制器之类的东西。但我相信瘦的一切方法,因为没有一个班应该是胖的,永远。所有的网络通常都可以抽象为业务逻辑,因此我们应该有另一个层,我们可以把它放在那里。服务层是我们所需要的:

它封装应用程序的业务逻辑,控制事务并协调其操作实现中的响应。

In our MVC realm Service Layer is something like a mediator between domain model and controllers. There is a rather similar variation of this approach called MVCS where a Store is actually our Service layer. Store vends model instances and handles the networking, caching etc. I want to mention that you should not write all your networking and business logic in your service layer. This also can be considered as a bad design. For more info look at the Anemic and Rich domain models. Some service methods and business logic can be handled in the model, so it will be a "rich" (with behaviour) model.

我经常广泛使用两个库:AFNetworking 2.0和ReactiveCocoa。我认为对于任何与网络和web服务交互或包含复杂UI逻辑的现代应用程序来说,它都是必须的。

体系结构

At first I create a general APIClient class, which is a subclass of AFHTTPSessionManager. This is a workhorse of all networking in the application: all service classes delegate actual REST requests to it. It contains all the customizations of HTTP client, which I need in the particular application: SSL pinning, error processing and creating straightforward NSError objects with detailed failure reasons and descriptions of all API and connection errors (in such case controller will be able to show correct messages for the user), setting request and response serializers, http headers and other network-related stuff. Then I logically divide all the API requests into subservices or, more correctly, microservices: UserSerivces, CommonServices, SecurityServices, FriendsServices and so on, accordingly to business logic they implement. Each of these microservices is a separate class. They, together, form a Service Layer. These classes contain methods for each API request, process domain models and always returns a RACSignal with the parsed response model or NSError to the caller.

I want to mention that if you have complex model serialisation logic - then create another layer for it: something like Data Mapper but more general e.g. JSON/XML -> Model mapper. If you have cache: then create it as a separate layer/service too (you shouldn't mix business logic with caching). Why? Because correct caching layer can be quite complex with its own gotchas. People implement complex logic to get valid, predictable caching like e.g. monoidal caching with projections based on profunctors. You can read about this beautiful library called Carlos to understand more. And don't forget that Core Data can really help you with all caching issues and will allow you to write less logic. Also, if you have some logic between NSManagedObjectContext and server requests models, you can use Repository pattern, which separates the logic that retrieves the data and maps it to the entity model from the business logic that acts on the model. So, I advice to use Repository pattern even when you have a Core Data based architecture. Repository can abstract things, like NSFetchRequest,NSEntityDescription, NSPredicate and so on to plain methods like get or put.

After all these actions in the Service layer, caller (view controller) can do some complex asynchronous stuff with the response: signal manipulations, chaining, mapping, etc. with the help of ReactiveCocoa primitives , or just subscribe to it and show results in the view. I inject with the Dependency Injection in all these service classes my APIClient, which will translate a particular service call into corresponding GET, POST, PUT, DELETE, etc. request to the REST endpoint. In this case APIClient is passed implicitly to all controllers, you can make this explicit with a parametrised over APIClient service classes. This can make sense if you want to use different customisations of the APIClient for particular service classes, but if you ,for some reasons, don't want extra copies or you are sure that you always will use one particular instance (without customisations) of the APIClient - make it a singleton, but DON'T, please DON'T make service classes as singletons.

Then each view controller again with the DI injects the service class it needs, calls appropriate service methods and composes their results with the UI logic. For dependency injection I like to use BloodMagic or a more powerful framework Typhoon. I never use singletons, God APIManagerWhatever class or other wrong stuff. Because if you call your class WhateverManager, this indicates than you don't know its purpose and it is a bad design choice. Singletons is also an anti-pattern, and in most cases (except rare ones) is a wrong solution. Singleton should be considered only if all three of the following criteria are satisfied:

单个实例的所有权不能合理分配的; 延迟初始化是可取的; 另外没有提供全局访问。

在我们的例子中,单个实例的所有权不是问题,而且在我们将上帝管理器划分为服务之后,我们也不需要全局访问,因为现在只有一个或几个专用控制器需要特定的服务(例如UserProfile控制器需要UserServices等等)。

我们应该始终尊重SOLID中的S原则,并使用关注点分离,所以不要将所有的服务方法和网络调用放在一个类中,因为这很疯狂,特别是如果您开发的是大型企业应用程序。这就是为什么我们应该考虑依赖注入和服务方法。我认为这种方法是现代的、后面向对象的。在本例中,我们将应用程序分为两部分:控制逻辑(控制器和事件)和参数。

一种参数是普通的“数据”参数。这就是我们传递函数、操作、修改、持久化等的方法。它们是实体、聚合、集合和案例类。另一种是“服务”参数。这些类封装业务逻辑,允许与外部系统通信,提供数据访问。

下面是我的体系结构的一般工作流示例。假设我们有一个FriendsViewController,它显示用户的好友列表,我们有一个选项可以从好友中删除。我在我的FriendsServices类中创建了一个方法:

- (RACSignal *)removeFriend:(Friend * const)friend

where Friend is a model/domain object (or it can be just a User object if they have similar attributes). Underhood this method parses Friend to NSDictionary of JSON parameters friend_id, name, surname, friend_request_id and so on. I always use Mantle library for this kind of boilerplate and for my model layer (parsing back and forward, managing nested object hierarchies in JSON and so on). After parsing it calls APIClient DELETE method to make an actual REST request and returns Response in RACSignal to the caller (FriendsViewController in our case) to display appropriate message for the user or whatever.

If our application is a very big one, we have to separate our logic even clearer. E.g. it is not *always* good to mix `Repository` or model logic with `Service` one. When I described my approach I had said that `removeFriend` method should be in the `Service` layer, but if we will be more pedantic we can notice that it better belongs to `Repository`. Let's remember what Repository is. Eric Evans gave it a precise description in his book [DDD]: A Repository represents all objects of a certain type as a conceptual set. It acts like a collection, except with more elaborate querying capability. So, a Repository is essentially a facade that uses Collection style semantics (Add, Update, Remove) to supply access to data/objects. That's why when you have something like: getFriendsList, getUserGroups, removeFriend you can place it in the Repository, because collection-like semantics is pretty clear here. And code like: - (RACSignal *)approveFriendRequest:(FriendRequest * const)request; is definitely a business logic, because it is beyond basic CRUD operations and connect two domain objects (Friend and Request), that's why it should be placed in the Service layer. Also I want to notice: don't create unnecessary abstractions. Use all these approaches wisely. Because if you will overwhelm your application with abstractions, this will increase its accidental complexity, and complexity causes more problems in software systems than anything else I describe you an "old" Objective-C example but this approach can be very easy adapted for Swift language with a lot more improvements, because it has more useful features and functional sugar. I highly recommend to use this library: Moya. It allows you to create a more elegant APIClient layer (our workhorse as you remember). Now our APIClient provider will be a value type (enum) with extensions conforming to protocols and leveraging destructuring pattern matching. Swift enums + pattern matching allows us to create algebraic data types as in classic functional programming. Our microservices will use this improved APIClient provider as in usual Objective-C approach. For model layer instead of Mantle you can use ObjectMapper library or I like to use more elegant and functional Argo library. So, I described my general architectural approach, which can be adapted for any application, I think. There can be a lot more improvements, of course. I advice you to learn functional programming, because you can benefit from it a lot, but don't go too far with it too. Eliminating excessive, shared, global mutable state, creating an immutable domain model or creating pure functions without external side-effects is, generally, a good practice, and new Swift language encourages this. But always remember, that overloading your code with heavy pure functional patterns, category-theoretical approaches is a bad idea, because other developers will read and support your code, and they can be frustrated or scary of the prismatic profunctors and such kind of stuff in your immutable model. The same thing with the ReactiveCocoa: don't RACify your code too much, because it can become unreadable really fast, especially for newbies. Use it when it can really simplify your goals and logic. So, read a lot, mix, experiment, and try to pick up the best from different architectural approaches. It is the best advice I can give you.