用于构建iOS网络应用程序(REST客户端)的最佳架构方法


323

我是一位有一定经验的iOS开发人员,这个问题对我来说真的很有趣。我在这个主题上看到了很多不同的资源和资料,但是我仍然感到困惑。iOS联网应用程序的最佳架构是什么?我的意思是说,基本的抽象框架,模式将适合每个网络应用程序,无论它是只有几个服务器请求还是复杂的REST客户端的小型应用程序。苹果公司建议将其MVC用作所有iOS应用程序的基本体系结构方法,但MVC更现代的MVVM模式都无法解释在何处放置网络逻辑代码以及通常如何组织网络逻辑代码。

我是否需要开发类似MVCSSfor Service)的东西,并在这Service一层中放入所有API请求和其他网络逻辑,所以它们实际上可能很复杂?经过研究,我发现了两种基本方法。在这里,建议为每个对Web服务的网络请求API(如LoginRequest类或PostCommentRequest类等)创建一个单独的类,这些类均继承自基本请求抽象类AbstractBaseRequest,此外,还建议创建一些封装通用网络代码的全局网络管理器,其他首选项(可以是AFNetworking自定义或RestKit调整,如果我们有复杂的对象映射和持久性,甚至是自己的使用标准API的网络通信实现)。但是这种方法对我来说似乎是开销。另一种方法是API像第一种方法那样具有一些单例调度程序或管理器类,但不为每个请求创建类,而是将每个请求封装为该管理器类的实例公共方法,例如fetchContactsloginUser方法等。最好和正确的方法?还有其他我不知道的有趣方法吗?

我是否应该为所有这些网络事物(例如Service,或NetworkProvider层)或我的MVC体系结构之上的其他层创建另一个层,或者应该将该层集成(注入)到现有MVC层中,例如Model

我知道有很漂亮的方法,或者像Facebook客户端或LinkedIn客户端这样的移动怪物如何应对网络逻辑的指数级增长?

我知道对这个问题没有确切和正式的答案。这个问题的目的是从经验丰富的iOS开发人员那里收集最有趣的方法。最好的建议方法将被标记为已接受并获得声誉奖励,其他将被推荐。这主要是一个理论和研究问题。我想了解iOS中联网应用程序的基本,抽象和正确的体系结构方法。我希望有经验的开发人员提供详细的解释。


14
这不是“购物清单”问题吗?我只是有一个问题被否决并关闭,因为有人说“什么是最好的”类型的问题引发了太多无建设性的辩论。是什么让这个购物清单问题成为一个值得投票和赏金的好问题,而其他人则被关闭?
艾文·汤普森

1
通常,网络逻辑将进入控制器,控制器将更改模型对象并通知任何委托人或观察者。
2014年

1
非常有趣的问题和答案。经过4年的iOS编码,并尝试找到最漂亮的方法向应用程序添加网络层。哪一类应负责管理网络请求?以下答案确实是相关的。谢谢
darksider

@JoeBlow这是不正确的。移动应用行业仍然非常依赖服务器-客户端通信。
斯考德

Answers:


327

I want to understand basic, abstract and correct architectural approach for networking applications in iOS没有用于构建应用程序体系结构的“最佳”或“最正确”方法。这是一项非常有创意的工作。您应该始终选择最直接和可扩展的体系结构,这对于任何开始从事您的项目的开发人员或团队中的其他开发人员来说都是显而易见的,但是我同意,可能存在“好”和“坏”的情况。 ”架构。

您说:collect the most interesting approaches from experienced iOS developers,我认为我的方法不是最有趣或最正确的,但是我已经在多个项目中使用了它,并对它感到满意。它是您上面提到的方法的一种混合方法,并且根据我自己的研究工作得到了改进。我对构建方法的问题很感兴趣,该方法结合了几种著名的模式和习惯用法。我认为许多Fowler的企业模式都可以成功地应用于移动应用程序。这是最有趣的列表,我们可以将其用于创建iOS应用程序体系结构(我认为):服务层工作单元远程外观数据传输对象网关 LevelDB层超类型特例领域模型。您应该始终正确设计模型层,并且永远不要忘记持久性(它可以显着提高应用程序的性能)。您可以Core Data为此使用。但是您不要忘记,它Core Data不是ORM或数据库,而是具有持久性的对象图管理器是不错的选择。因此,通常Core Data情况下,对于您的需求而言可能过于繁琐,您可以查看诸如RealmCouchbase Lite之类的新解决方案,或者基于原始SQLite或构建自己的轻量级对象映射/持久层。另外,我建议您熟悉域驱动设计CQRS

首先,我认为,我们应该创建另一个网络层,因为我们不想要笨拙的控制器或笨拙的模型。我不相信那些fat model, skinny controller东西。但我相信skinny everything方法,因为不用上课应该是脂肪,永远。通常可以将所有网络抽象为业务逻辑,因此,我们应该在其中放置另一层。服务层是我们需要的:

It encapsulates the application's business logic,  controlling transactions 
and coordinating responses in the implementation of its operations.

在我们的MVC领域Service Layer中,就像领域模型和控制器之间的中介者一样。此方法有一个非常类似的变体,称为MVCS,其中a Store实际上是我们的Service层。Store出售模型实例并处理网络,缓存等。我想提一提,您不应在服务层中编写所有网络和业务逻辑。这也可以认为是错误的设计。有关更多信息,请查看AnemicRich域模型。可以在模型中处理某些服务方法和业务逻辑,因此它将是一个“丰富的”(具有行为)模型。

我总是广泛使用两个库:AFNetworking 2.0ReactiveCocoa。我认为这是与网络和Web服务交互或包含复杂UI逻辑的任何现代应用程序所必需的

建筑

首先,我创建一个通用APIClient类,它是AFHTTPSessionManager的子类。这是应用程序中所有网络的主力军:所有服务类都将实际的REST请求委托给它。它包含HTTP客户端的所有自定义设置,我在特定应用程序中需要这些自定义设置:SSL固定,错误处理以及创建NSError具有详细失败原因以及所有API错误和连接错误的描述的简单对象(在这种情况下,控制器将能够显示有关以下内容的正确消息)用户),设置请求和响应序列化程序,http标头和其他与网络相关的内容。然后,我在逻辑上将所有的API请求转换的子服务,或者更确切地说,是微服务UserSerivcesCommonServicesSecurityServicesFriendsServices依其实现的业务逻辑等等。这些微服务中的每一个都是一个单独的类。它们一起形成一个Service Layer。这些类包含用于每个API请求的方法,过程域模型,并始终将RACSignal带有已解析的响应模型或NSError返回给调用方。

我想提到的是,如果您具有复杂的模型序列化逻辑,请为其创建另一个层:类似于Data Mapper,但更通用,例如JSON / XML-> Model mapper。如果您有缓存:那么也将其创建为单独的层/服务(您不应将业务逻辑与缓存混在一起)。为什么?因为正确的缓存层本身就很复杂。人们实施复杂的逻辑来获得有效的,可预测的缓存,例如基于特征符的带有投影的单面缓存。您可以阅读有关这个​​名为Carlos的美丽图书馆的信息,以了解更多信息。而且不要忘了Core Data确实可以帮助您解决所有缓存问题,并减少编写逻辑。另外,如果您NSManagedObjectContext和服务器请求模型之间有某种逻辑,则可以使用存储库模式,该模式将检索数据并将其映射到实体模型的逻辑与作用在模型上的业务逻辑分开。因此,即使您具有基于Core Data的体系结构,我也建议您使用Repository模式。仓库可以抽象的东西,比如NSFetchRequestNSEntityDescriptionNSPredicate等为普通方法,如getput

在Service层中完成所有这些操作之后,调用者(视图控制器)可以使用响应来执行一些复杂的异步操作:借助ReactiveCocoa原语,进行信号操作,链接,映射等操作,或者仅订阅它并在视图中显示结果。我注入与依赖注入在所有这些服务类我APIClient,这将转化特定服务调用成相应的GETPOSTPUTDELETE,等请求REST端点。在这种情况下APIClient,将隐式传递给所有控制器,您可以通过对APIClient服务类进行参数化来使其显式。如果您想使用不同的自定义APIClient适用于特定的服务类,但是如果出于某些原因,您不希望获得额外的副本,或者您可以确定始终使用(不进行自定义的)特定实例APIClient-将其设为单例,但是请不要'不要将服务类作为单例。

然后,每个具有DI的视图控制器再次注入所需的服务类,调用适当的服务方法,并将其结果与UI逻辑组合在一起。对于依赖注入,我喜欢使用BloodMagic或更强大的Typhoon框架。我从不使用单身人士,神职人员APIManagerWhatever或其他错误的东西。因为如果你打电话给班级WhateverManager,这表明你不知道班级的目的,这是一个错误的设计选择。Singletons也是一种反模式,在大多数情况下(罕见情况除外)是错误的解决方案。仅当满足以下所有三个条件时,才应考虑单例:

  1. 无法合理分配单个实例的所有权;
  2. 延迟初始化是可取的。
  3. 否则不提供全局访问。

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

我们应该始终S遵循SOLID中的原则并使用关注点分离,因此不要将所有服务方法和网络调用归为一类,因为这很疯狂,尤其是在开发大型企业应用程序时。这就是为什么我们应该考虑依赖注入和服务方法。我认为这种方法是现代且面向对象的。在这种情况下,我们将应用程序分为两部分:控制逻辑(控制器和事件)和参数。

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

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

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

Friend模型/领域对象在哪里(User如果它们具有相似的属性,则可以只是一个对象)。引擎盖下此方法分析Friend,以NSDictionaryJSON的参数friend_idnamesurnamefriend_request_id等等。我总是将Mantle库用于此类样板和模型层(前后解析,在JSON中管理嵌套的对象层次结构等)。解析后它调用APIClient DELETE方法以使实际的REST请求并返回ResponseRACSignal给调用者(FriendsViewController在我们的例子),以供用户或任何显示适当的消息。

如果我们的应用程序很大,那么我们就必须将逻辑更加清晰地分开。例如,将逻辑与一个逻辑混合或建模并不总是一件好事。当我描述我的方法时,我曾说过该方法应该在层中,但是如果我们要花很多精力,我们会注意到它更好地属于。让我们记住什么是存储库。埃里克·埃文斯(Eric Evans)在他的书[DDD]中给出了准确的描述:RepositoryServiceremoveFriendServiceRepository

存储库将某种类型的所有对象表示为概念集。除了具有更复杂的查询功能外,它的作用类似于集合。

因此,a Repository本质上是一个使用Collection样式语义(添加,更新,删除)提供对数据/对象的访问的外观。这就是为什么当你碰到这样的:getFriendsListgetUserGroupsremoveFriend你可以将其放置在Repository,因为集合类语义是相当清楚的在这里。和类似的代码:

- (RACSignal *)approveFriendRequest:(FriendRequest * const)request;

绝对是一种业务逻辑,因为它超出了基本CRUD操作,并且连接了两个域对象(FriendRequest),这就是为什么应将其放在Service层中。我还要注意:不要创建不必要的抽象。明智地使用所有这些方法。因为如果您要用抽象来淹没您的应用程序,这将增加其偶然的复杂性,并且复杂性在软件系统中 引起更多的问题

我为您描述了一个“旧的” Objective-C示例,但是这种方法可以很容易地应用于Swift语言,并进行了很多改进,因为它具有更多有用的功能和功能。我强烈建议使用以下库:Moya。它使您可以创建更优雅的APIClient图层(您记得的是我们的主力军)。现在,我们的APIClient提供程序将是一个值类型(枚举),其扩展名符合协议并利用解构模式匹配。Swift枚举+模式匹配使我们能够像经典函数式编程一样创建代数数据类型。我们的微服务将APIClient像通常的Objective-C方法一样使用此改进的提供程序。对于模型层,Mantle您可以使用 ObjectMapper库或者我喜欢使用更优雅,更实用的Argo库。

因此,我描述了我的通用体系结构方法,我认为它可以适用于任何应用程序。当然,还有很多改进。我建议您学习函数式编程,因为您可以从中受益匪浅,但也不要太过分。消除过多的,共享的,全局可变的状态,创建不可变的域模型或创建没有外部副作用的纯函数通常是一种好习惯,并且新Swift语言鼓励这样做。但是请始终记住,使用繁重的纯函数模式,基于类理论的方法来使代码重载是一个主意,因为其他开发人员将阅读并支持您的代码,并且他们可能对此感到沮丧或恐惧。的代码prismatic profunctors以及不可变模型中的这类内容。与ReactiveCocoa不太相同,因为它很快就会变得难以阅读,尤其是对于新手。当它可以真正简化您的目标和逻辑时,请使用它。 RACify

因此,read a lot, mix, experiment, and try to pick up the best from different architectural approaches。这是我能给您的最佳建议。


也是一种有趣且可靠的方法。谢谢。
MainstreamDeveloper00 2014年

1
@darksider正如我已经写在我的答案是:“'我从来没有使用单身,神APIManagerWhatever类或其它错误的东西,因为单是一个反模式,在大多数情况下(除了罕见的)是一个错误的解决方案。". I don't like singletons. I have an opinion that if you decided to use singletons in your project you should have at least three criteria why you do this (I edited my answer). So I inject them (lazy of course and not each time, but 一旦`)在每一个控制器。
奥列克Karaberov

14
嗨@alexander 您在GitHub上有任何示例项目吗?您描述了一种非常有趣的方法。谢谢。但是我是Objective-C开发的初学者。对于我来说,有些方面很难理解。也许您可以在GitHub上上传一些测试项目并提供链接?
丹尼斯

1
您好@AlexanderKaraberov,对于您提供的商店说明,我有些困惑。假设我有5个模型,每个模型有2个类,一个类维护对象的联网和其他缓存。现在,对于每个模型,我应该为网络和缓存类调用单独的Store类,还是对每个模型具有全部功能的单个Store类,因此控制器始终访问单个文件以获取数据。
流星

1
@icodebuster这个演示项目一直在帮助我理解此处概述的许多概念:github.com/darthpelo/NetworkLayerExample

31

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

架构方法

我们一般的iOS应用程序的体系结构基于以下模式:服务层MVVMUI数据绑定依赖注入;和功能反应式编程范例。

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

  • 部件
  • 模型
  • 服务
  • 存储
  • 管理人员
  • 协调员
  • 用户界面
  • 基础设施

汇编层是我们应用程序的引导点。它包含一个依赖注入容器以及应用程序对象及其依赖关系的声明。该层还可能包含应用程序的配置(URL,第三方服务密钥等)。为此,我们使用台风库。

模型层包含领域模型类,验证,映射。我们使用Mantle库来映射模型:它支持将序列化/反序列化为JSON格式和NSManagedObject模型。为了验证和验证模型的形式,我们使用FXFormsFXModelValidation库。

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

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

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

在经理角色中可以是:

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

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

我们尝试避免使用Singletons,但如果需要,此层是它们生活的地方。

协调器层提供的对象取决于其他层(服务,存储,模型)的对象,以便将其逻辑组合为某些模块(功能,屏幕,用户故事或用户体验)所需的一系列工作。它通常链接异步操作,并且知道如何对成功和失败的情况做出反应。例如,您可以想象一个消息传递功能和相应的MessagingCoordinator对象。处理发送消息的操作可能如下所示:

  1. 验证消息(模型层)
  2. 在本地保存消息(消息存储)
  3. 上传邮件附件(amazon s3服务)
  4. 更新邮件状态和附件网址,并在本地保存邮件(邮件存储)
  5. 将消息序列化为JSON格式(模型层)
  6. 将消息发布到PubNub(PubNub服务)
  7. 更新消息状态和属性并将其保存在本地(消息存储)

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

UI层包含以下子层:

  1. 视图模型
  2. ViewControllers
  3. 观看次数

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

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

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

基础结构层包含应用程序工作所需的所有帮助程序,扩展,实用程序。


这种方法对我们以及我们通常构建的那些类型的应用程序都适用。但是您应该理解,这只是一个主观方法,应该针对具体团队的目的进行调整/更改。

希望这能够帮到你!

您也可以在此博客文章iOS开发即服务中找到有关iOS开发过程的更多信息。


几个月前开始喜欢这种架构,感谢Alex的分享!我想在不久的将来与RxSwift一起尝试!
ingaham'8

18

因为所有iOS应用程序都不同,所以我认为这里有不同的方法要考虑,但是我通常采用这种方式:
创建一个中央管理器(单例)类来处理所有API请求(通常称为APICommunicator),并且每个实例方法都是一个API调用。有一种中央(非公开)方法:

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

作为记录,我使用了2个主要的库/框架,ReactiveCocoa和AFNetworking。ReactiveCocoa可以完美地处理异步网络响应,您可以做到(sendNext:,sendError:等)。
该方法调用API,获取结果,然后以“原始”格式(如NSArray AFNetworking返回的结果)通过RAC发送结果。
然后一个getStuffList:称为上述方法的方法订阅其信号,将原始数据解析为对象(使用Motis之类的对象),然后将对象逐个发送给调用方(getStuffList:类似的方法也返回信号,表明控制器可以订阅)。
订阅的控制器通过subscribeNext:的块接收对象并进行处理。

我在不同的应用程序中尝试了多种方法,但是这种方法在所有应用程序中都发挥了最好的作用,因此最近我在一些应用程序中都使用了这种方法,它适合大型和小型项目,并且如果需要修改某些内容,则很容易扩展和维护。
希望这会有所帮助,我想听听其他人对我的方法的看法,也许其他人认为这可能会得到改善。


2
感谢您的答案+1。好方法。我离开这个问题。也许我们会从其他开发人员那里获得其他一些方法。
MainstreamDeveloper00 2014年

1
我喜欢这种方法的一种变化-我使用一个中央API管理器,该管理器负责与API通信的机制。但是,我尝试将所有功能公开在模型对象上。模型将提供+ (void)getAllUsersWithSuccess:(void(^)(NSArray*))success failure:(void(^)(NSError*))failure;和方法,- (void)postWithSuccess:(void(^)(instancetype))success failure:(void(^)(NSError*))failure;这些方法会做必要的准备,然后调用API管理器。
jsadler 2014年

1
这种方法很简单,但是随着API数量的增加,维护单例API管理器变得越来越困难。而且,无论该API属于哪个模块,每个新添加的API都将与管理器相关。尝试使用github.com/kevin0571/STNetTaskQueue来管理API请求。
凯文(Kevin)

除了您为什么要宣传您的图书馆(离我的解决方案尽可能远,而且要复杂得多)的观点之外,我已经在如上所述的无数大小项目中尝试了这种方法,并且我一直在使用它。自从我写了这个答案以来就一样。有了聪明的命名约定,一点都不难维护。
Rickye 2015年

8

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

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

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

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

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实现的一些示例:

用户名

@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

用户名

@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.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

只需编写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上的某个地方为它制定一个通用解决方案。


7

在我看来,所有软件体系结构都是由需求驱动的。如果这是出于学习或个人目的,则确定主要目标并由其驱动体系结构。如果这是要出租的工作,那么业务需求至关重要。诀窍是不要让闪亮的事物分散您的实际需求。我觉得这很难做。在这项业务中总会出现新的闪亮事物,其中许多没有用,但您始终无法始终将其告知。专注于需求,如果可以的话,愿意放弃错误的选择。

例如,我最近为本地企业制作了一个照片共享应用程序的快速原型。由于业务需求是快速而又肮脏地做某事,因此该体系结构最终是一些用于弹出摄像头的iOS代码以及一些附加到“发送按钮”的网络代码,该按钮将图像上传到S3存储并写入SimpleDB域。该代码非常简单,成本最低,并且客户端具有可扩展的照片集,可通过REST调用在网络上访问。该应用程序便宜又笨拙,存在很多缺陷,有时会锁定用户界面,但是对原型进行更多的工作是浪费的,它允许他们将其部署到其员工并轻松生成数千个测试图像而没有性能或可伸缩性关注。笨拙的体系结构,但它完全可以满足需求,并且价格合理。

另一个项目涉及实现本地安全数据库,当网络可用时,该安全数据库在后台与公司系统同步。我创建了一个使用RestKit的后台同步器,因为它似乎拥有我所需的一切。但是我不得不为RestKit写这么多自定义代码来处理特有的JSON,通过将自己的JSON写入CoreData转换,我可以更快地完成所有工作。但是,客户希望将这个应用程序带到内部,我觉得RestKit类似于他们在其他平台上使用的框架。我在等那是个好决定。

再次,对我来说,问题是将重点放在需求上,并由它确定体系结构。我尽力避免使用第三方软件包,因为它们带来的费用仅在应用程序进入现场一段时间后才会出现。我尽量避免建立类层次结构,因为它们很少能得到回报。如果我可以在合理的时间内写一些东西,而不是采用不合适的软件包,那我就做。我的代码结构良好,可用于调试并带有适当的注释,但很少有第三方软件包。话虽如此,我发现AF Networking太有用了,无法忽略并且结构合理,评论良好并易于维护,因此我经常使用它!RestKit涵盖了很多常见情况,但是我觉得我在使用它时一直在战斗中,而且我遇到的大多数数据源都充满了怪异和问题,这些问题最好用自定义代码处理。在我的最后几个应用程序中,我仅使用内置的JSON转换器并编写一些实用程序方法。

我一直使用的一种模式是使网络调用脱离主线程。我完成的最后4-5个应用程序使用dispatch_source_create设置了一个后台计时器任务,该任务每隔一会醒来一次,并根据需要执行网络任务。您需要做一些线程安全工作,并确保将UI修改代码发送到主线程。它还有助于以不让用户感到负担或延迟的方式进行您的入职/初始化。到目前为止,它一直运行良好。我建议调查这些事情。

最后,我认为随着工作量的增加以及操作系统的发展,我们倾向于开发更好的解决方案。我花了多年的时间才能克服我的信念,即我必须遵循别人声称是强制性的模式和设计。如果我是在当地宗教的背景下工作,那么,我的意思是部门的最佳工程实践,那么我就按照惯例来写信,这就是他们为此付出的代价。但是我很少发现遵循旧的设计和模式是最佳的解决方案。我总是尝试从业务需求的角度来研究解决方案,并构建与之匹配的体系结构,并使事情尽可能地简单。当我觉得那里没有足够的东西,但一切正常时,那我就走对了。


4

我使用从这里获得的方法:https : //github.com/Constantine-Fry/Foursquare-API-v2。我已经在Swift中重写了该库,您可以从代码的以下部分看到架构方法:

typealias OpertaionCallback = (success: Bool, result: AnyObject?) -> ()

class Foursquare{
    var authorizationCallback: OperationCallback?
    var operationQueue: NSOperationQueue
    var callbackQueue: dispatch_queue_t?

    init(){
        operationQueue = NSOperationQueue()
        operationQueue.maxConcurrentOperationCount = 7;
        callbackQueue = dispatch_get_main_queue();
    }

    func checkIn(venueID: String, shout: String, callback: OperationCallback) -> NSOperation {
        let parameters: Dictionary <String, String> = [
            "venueId":venueID,
            "shout":shout,
            "broadcast":"public"]
        return self.sendRequest("checkins/add", parameters: parameters, httpMethod: "POST", callback: callback)
    }

    func sendRequest(path: String, parameters: Dictionary <String, String>, httpMethod: String, callback:OperationCallback) -> NSOperation{
        let url = self.constructURL(path, parameters: parameters)
        var request = NSMutableURLRequest(URL: url)
        request.HTTPMethod = httpMethod
        let operation = Operation(request: request, callbackBlock: callback, callbackQueue: self.callbackQueue!)
        self.operationQueue.addOperation(operation)
        return operation
    }

    func constructURL(path: String, parameters: Dictionary <String, String>) -> NSURL {
        var parametersString = kFSBaseURL+path
        var firstItem = true
        for key in parameters.keys {
            let string = parameters[key]
            let mark = (firstItem ? "?" : "&")
            parametersString += "\(mark)\(key)=\(string)"
            firstItem = false
        }
    return NSURL(string: parametersString.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding))
    }
}

class Operation: NSOperation {
    var callbackBlock: OpertaionCallback
    var request: NSURLRequest
    var callbackQueue: dispatch_queue_t

    init(request: NSURLRequest, callbackBlock: OpertaionCallback, callbackQueue: dispatch_queue_t) {
        self.request = request
        self.callbackBlock = callbackBlock
        self.callbackQueue = callbackQueue
    }

    override func main() {
        var error: NSError?
        var result: AnyObject?
        var response: NSURLResponse?

        var recievedData: NSData? = NSURLConnection.sendSynchronousRequest(self.request, returningResponse: &response, error: &error)

        if self.cancelled {return}

        if recievedData{
            result = NSJSONSerialization.JSONObjectWithData(recievedData, options: nil, error: &error)
            if result != nil {
                if result!.isKindOfClass(NSClassFromString("NSError")){
                    error = result as? NSError
            }
        }

        if self.cancelled {return}

        dispatch_async(self.callbackQueue, {
            if (error) {
                self.callbackBlock(success: false, result: error!);
            } else {
                self.callbackBlock(success: true, result: result!);
            }
            })
    }

    override var concurrent:Bool {get {return true}}
}

基本上,有一个NSOperation子类可以创建NSURLRequest,解析JSON响应并将带有结果的回调块添加到队列中。主API类构造NSURLRequest,初始化该NSOperation子类并将其添加到队列中。


3

我们根据情况使用一些方法。对于大多数事情而言,AFNetworking是最简单,最可靠的方法,因为您可以设置标题,上传多部分数据,使用GET,POST,PUT和DELETE,并且UIKit还有很多其他类别,例如,您可以从一个网址。在具有大量调用的复杂应用程序中,有时我们会将其抽象为我们自己的便捷方法,该方法类似于:

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

在某些情况下,AFNetworking不适合使用,例如您正在创建框架或其他库组件,因为AFNetworking可能已经在另一个代码库中。在这种情况下,如果要进行单个调用,则可以内联使用NSMutableURLRequest,也可以抽象为请求/响应类。


对我来说,这是最好,最清晰的答案,欢呼。“就是这么简单”。@martin,就我们个人而言,我们一直都在使用NSMutableURLRequest;是否有使用AFNetworking的真正理由?
Fattie

AFNetworking真的很方便。对我来说,成功和失败块值得,因为它使代码更易于管理。我同意,尽管有时这完全是矫kill过正。
马丁

非常感谢,谢谢。我猜想,这一切的具体性质都将随着Swift而改变。
Fattie

2

在设计应用程序时,我避免单例。对于许多人来说,它们是典型的选择,但是我认为您可以在其他地方找到更优雅的解决方案。通常,我要做的是在CoreData中构建实体,然后将REST代码放入NSManagedObject类别中。例如,如果我想创建并发布一个新用户,则可以这样做:

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];
}

当您可以通过类别扩展通用基类的功能时,为什么还要添加额外的帮助程序类?

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


3
绝对有兴趣在博客文章中详细了解这种方法。
Danyal Aytekin


0

从纯类设计的角度来看,您通常会遇到以下情况:

  • 您的视图控制器控制一个或多个视图
  • 数据模型类 -实际上取决于您要处理多少个真正的不同实体以及它们之间的关系。

    例如,如果有一组要以四种不同表示形式(列表,图表,图形等)显示的项目,则将有一个数据模型类用于项目列表,另外一个用于项目。项目类别列表将由四个视图控制器共享-标签栏控制器或导航控制器的所有子级。

    数据模型类不仅可以方便地显示数据,还可以对其进行序列化,其中每个模型都可以通过JSON / XML / CSV(或其他任何一种)导出方法公开自己的序列化格式。

  • 重要的是要了解,您还需要直接与REST API端点映射的API请求构建器类。假设您有一个用于登录用户的API-因此,“ Login API”构建器类将为登录api创建POST JSON有效负载。在另一个示例中,用于商品目录API的API请求构建器类将为相应的api创建GET查询字符串,并触发REST GET查询。

    这些API请求构建器类通常将从视图控制器接收数据,并将相同的数据传回视图控制器以进行UI更新/其他操作。然后,视图控制器将决定如何使用该数据更新数据模型对象。

  • 最后,在REST客户端的心脏 - API数据提取器类是无视各种API的请求您的应用更。此类很可能是单身人士,但正如其他人指出的那样,它不一定是单身人士。

    请注意,该链接只是一个典型的实现,并没有考虑会话,Cookie等场景,但这足以使您无需使用任何第三者框架。


0

这个问题已经有很多出色而广泛的答案,但是我觉得我不得不提这个问题,因为没有其他人知道。

Swift的Alamofire。https://github.com/Alamofire/Alamofire

它是由与AFNetworking相同的人员创建的,但更直接地考虑了Swift的设计。


0

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

  • 面向协议的编程
  • 软件设计模式
  • 卖出原则
  • 通用编程
  • 不要重复自己(干燥)

以及用于构建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()
        }
    }

您将在这里找到带有其余API Swift项目的GitHub MVVM架构


0

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

清洁架构+ MVVM包含3个层:域,表示,数据层。表示层和数据存储库层取决于域层的位置:

Presentation Layer -> Domain Layer <- Data Repositories Layer

表示层由ViewModels和Views(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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.