我是一位有经验的iOS开发人员,这个问题对我来说非常有趣。我在这个主题上看到了很多不同的资源和材料,但我仍然感到困惑。 iOS网络应用程序的最佳架构是什么?我的意思是基本的抽象框架,模式,它适合每个网络应用程序,无论它是一个只有少量服务器请求的小应用程序还是复杂的REST客户端。 Apple建议将MVC
用作所有iOS应用程序的基本架构方法,但MVC
和更现代的MVVM
模式都不能解释网络逻辑代码的放置位置以及如何组织它。
我是否需要开发类似MVCS
(S
Service
)的内容,并在此Service
图层中添加所有API
个请求其他网络逻辑,从视角来看可能真的很复杂?在做了一些研究后,我发现了两种基本方法。 Here建议为每个网络服务API
(如LoginRequest
类或PostCommentRequest
类等)的网络请求创建一个单独的类,这些类都继承自基本请求抽象类AbstractBaseRequest
以及创建一些全局网络管理器,它封装了常见的网络代码和其他首选项(可能是AFNetworking
自定义或RestKit
调优,如果我们有复杂的对象映射和持久性,甚至是使用标准API的自己的网络通信实现。但这种方法对我来说似乎是一个开销。另一种方法是在第一种方法中使用一些单例API
调度程序或管理器类,但不是为每个请求创建类,而是将每个请求封装为此实例的公共方法经理类如:fetchContacts
,loginUser
方法等等。那么,最好和最正确的方法是什么?还有其他有趣的方法我还不知道吗?
我是否应该为所有这些网络内容创建另一个层,例如Service
或NetworkProvider
图层或我的MVC
架构之上的任何内容,或者此层应该是集成(注入)现有的MVC
层,例如Model
?
我知道存在很好的方法,或者像Facebook客户端或LinkedIn客户端这样的移动怪物如何应对指数级增长的网络逻辑复杂性?
我知道这个问题没有确切而正式的答案。 此问题的目标是从经验丰富的iOS开发人员那里收集最有趣的方法。最佳建议方法将被标记为已接受并获得声誉奖励,其他方式将被赞成。这主要是一个理论和研究问题。我想了解iOS中网络应用程序的基本,抽象和正确的架构方法。我希望有经验的开发人员提供详细解释。
答案 0 :(得分:315)
I want to understand basic, abstract and correct architectural approach for networking applications in iOS
:没有"最好的"或者#34;最正确的"构建应用程序架构的方法。这是非常创意工作。您应该始终选择最直接和可扩展的体系结构,对于任何开始处理项目或开发团队中其他开发人员的开发人员来说都是明确的,但我同意,可以有一个" good&#34 ;和一个"坏"建筑。
你说:collect the most interesting approaches from experienced iOS developers
,我不认为我的方法是最有趣或最正确的,但我已经在几个项目中使用它并对此感到满意。它是您上面提到的那种混合方法,也是我自己研究工作的改进。我对构建方法的问题很感兴趣,它结合了几种众所周知的模式和习语。我认为很多Fowler's enterprise patterns可以成功应用于移动应用程序。以下是最有趣的列表,我们可以申请创建iOS应用程序架构(我认为 ):Service Layer,Unit Of Work,Remote Facade ,Data Transfer Object,Gateway,Layer Supertype,Special Case,Domain Model。您应始终正确设计模型图层,并始终不要忘记持久性(它可以显着提高您的应用程序的性能)。您可以使用Core Data
。但是你不应该忘记,Core Data
不是ORM或数据库,而是具有持久性的对象图管理器作为它的一个很好的选择。因此,Core Data
常常对您的需求来说过于沉重,您可以查看Realm和Couchbase Lite等新解决方案,或构建自己的轻量级对象映射/持久层,基于原始SQLite或LevelDB。另外,我建议您熟悉Domain Driven Design和CQRS。
首先,我认为,我们应该创建另一个网络层,因为我们不想要胖控制器或沉重,不堪重负的模型。我不相信那些fat model, skinny controller
事。但我在skinny everything
方法中相信,因为任何课程都不应该胖。所有网络通常都可以抽象为业务逻辑,因此我们应该有另一层,我们可以把它放在哪里。 Service Layer是我们所需要的:
It encapsulates the application's business logic, controlling transactions
and coordinating responses in the implementation of its operations.
在我们的MVC
领域Service Layer
类似于域模型和控制器之间的中介。这种方法的变体类似于MVCS,其中Store
实际上是我们的Service
层。 Store
出售模型实例并处理网络,缓存等。我想提一下,不应在服务层中编写所有网络和业务逻辑。这也可以被认为是一个糟糕的设计。有关详细信息,请查看Anemic和Rich域模型。一些服务方法和业务逻辑可以在模型中处理,因此它将是一个丰富的" (有行为)模型。
我总是广泛使用两个库:AFNetworking 2.0和ReactiveCocoa。我认为对于任何与网络和Web服务交互或包含复杂UI逻辑的现代应用程序来说,必须具有。
<强>建筑强>
首先,我创建了一个通用APIClient
类,它是AFHTTPSessionManager的子类。这是应用程序中所有网络的主力:所有服务类都将实际的REST请求委托给它。它包含HTTP客户端的所有自定义,我在特定应用程序中需要这些自定义:SSL固定,错误处理和创建简单的NSError
对象,其中包含详细的故障原因和所有API
的描述以及连接错误(在此类中)案例控制器将能够为用户显示正确的消息),设置请求和响应序列化程序,http标头和其他与网络相关的内容。然后我逻辑上将所有API请求划分为子服务,或者更准确地说,microservices:UserSerivces
,CommonServices
,SecurityServices
,FriendsServices
等等,相应地他们实施的业务逻辑。这些微服务中的每一个都是一个单独的类。他们一起组成Service Layer
。这些类包含每个API请求的方法,进程域模型,并始终将带有解析响应模型的RACSignal
或NSError
返回给调用者。
我想提一下,如果您有复杂的模型序列化逻辑 - 那么为它创建另一个层:类似于Data Mapper,但更常见,例如JSON / XML - &gt;模型映射器。如果你有缓存:那么也将它创建为一个单独的层/服务(你不应该将业务逻辑与缓存混合)。为什么?因为正确的缓存层可能与其自身的陷阱相当复杂。人们实现复杂的逻辑以获得有效的,可预测的缓存,例如基于profunctors的投影的monoidal缓存。您可以阅读有关这个名为Carlos的美丽图书馆以了解更多信息。并且不要忘记Core Data可以真正帮助您解决所有缓存问题,并且可以让您编写更少的逻辑。此外,如果NSManagedObjectContext
与服务器请求模型之间存在某种逻辑,则可以使用Repository模式,该模式将检索数据的逻辑与执行的业务逻辑中的实体模型分离。该模型。因此,即使您拥有基于Core Data的架构,我也建议使用Repository模式。存储库可以抽象诸如NSFetchRequest
,NSEntityDescription
,NSPredicate
等内容,以及get
或put
之类的简单方法。
在服务层中执行所有这些操作之后,调用者(视图控制器)可以使用响应执行一些复杂的异步操作:信号操作,链接,映射等,并借助ReactiveCocoa
原语,或者只是订阅它并在视图中显示结果。我在APIClient
所有这些服务类中注入Dependency Injection,这会将特定服务调用转换为相应的GET
,POST
,PUT
,{{ 1}}等请求REST端点。在这种情况下,DELETE
会隐式传递给所有控制器,您可以使用参数化的APIClient
服务类来显式显示它。如果您想对特定服务类使用APIClient
的不同自定义,这是有意义的,但如果您出于某些原因,不想要额外的副本,或者您确定总是会使用一个特定的APIClient
的实例(没有自定义) - 使其成为单身,但是请注意,请不要将服务类作为单身人士。
然后每个视图控制器再次使用DI注入所需的服务类,调用适当的服务方法并使用UI逻辑组合其结果。对于依赖注入,我喜欢使用BloodMagic或更强大的框架Typhoon。我从不使用单身人士,上帝APIClient
阶级或其他错误的东西。因为如果你打电话给你的班级APIManagerWhatever
,这表明你不知道它的目的而且是bad design choice。单身人士也是一种反模式,在大多数情况下(罕见的除外)是错误的解决方案。只有满足以下三个条件时才应考虑单身人士:
在我们的案例中,单个实例的所有权不是问题,在我们将神经理分成服务之后我们也不需要全局访问,因为现在只有一个或几个专用控制器需要特定服务(例如{ {1}}控制器需要WhateverManager
等等。
我们应始终尊重SOLID中的UserProfile
原则并使用separation of concerns,因此请勿将所有服务方法和网络调用放在一个类中,因为它很疯狂,特别是如果你开发一个大型企业应用程序。这就是我们应该考虑依赖注入和服务方法的原因。我认为这种方法是现代的post-OO。在这种情况下,我们将应用程序分为两部分:控制逻辑(控制器和事件)和参数。
一种参数是普通的“数据”参数。这就是我们传递函数,操作,修改,持久等等。这些是实体,聚合,集合,案例类。另一种是“服务”参数。这些类封装了业务逻辑,允许与外部系统通信,提供数据访问。
以下是我的架构的一般工作流程。我们假设我们有UserServices
,其中显示了用户朋友的列表,我们可以选择从朋友中删除。我在S
类中创建了一个名为:
FriendsViewController
其中FriendsServices
是模型/域对象(如果它们具有相似的属性,它可以只是- (RACSignal *)removeFriend:(Friend * const)friend
对象)。根据此功能,此方法会将Friend
解析为User
个JSON参数Friend
,NSDictionary
,friend_id
,name
等等。我总是将Mantle库用于这种模板和我的模型层(前后解析,在JSON中管理嵌套对象层次结构等)。解析之后调用surname
friend_request_id
方法来发出实际的REST请求并将APIClient
中的DELETE
返回给调用者(在我们的例子中是Response
)以显示适当的用户或其他任何消息。
如果我们的应用程序是一个非常大的应用程序,我们必须将我们的逻辑分离得更清楚。例如。将RACSignal
或模型逻辑与FriendsViewController
混合起来并不总是总是好。当我描述我的方法时,我曾说Repository
方法应该在Service
层,但如果我们更迂腐,我们可以注意到它更适合removeFriend
。让我们记住存储库是什么。 Eric Evans在他的书[DDD]中给出了一个精确的描述:
存储库将特定类型的所有对象表示为概念集。它的作用就像一个集合,除了更精细的查询功能。
因此,Service
本质上是一个使用集合样式语义(添加,更新,删除)来提供对数据/对象的访问的外观。这就是为什么当你有类似的东西时:Repository
,Repository
,getFriendsList
你可以将它放在getUserGroups
中,因为这里类似集合的语义非常清楚。代码如下:
removeFriend
肯定是一种业务逻辑,因为它超出了基本的Repository
操作并且连接了两个域对象(- (RACSignal *)approveFriendRequest:(FriendRequest * const)request;
和CRUD
),这就是为什么它应该放在Friend
图层。另外我想注意:不要创建不必要的抽象。明智地使用所有这些方法。因为如果你用抽象来压倒你的应用程序,这将增加它在软件系统中的意外复杂性和复杂性causes more problems而不是其他任何东西
我描述了一个&#34; old&#34; Objective-C示例,但这种方法非常容易适应Swift语言,并且有很多改进,因为它具有更多有用的功能和功能性糖。我强烈建议您使用此库:Moya。它允许您创建更优雅的Request
图层(我们记住的主力)。现在,我们的Service
提供程序将是一个值类型(枚举),其扩展符合协议并利用解构模式匹配。 Swift枚举+模式匹配允许我们像经典函数式编程一样创建algebraic data types。我们的微服务将使用这种改进的APIClient
提供程序,就像通常的Objective-C方法一样。对于模型层而不是APIClient
,您可以使用ObjectMapper library或者我喜欢使用更优雅且功能更强的Argo库。
所以,我想了解我的一般架构方法,可以适用于任何应用程序。当然,还有很多改进。我建议你学习函数式编程,因为你可以从中受益很多,但也不要太过分了。消除过度,共享,全局可变状态,创建immutable domain model或创建纯函数而没有外部副作用通常是一种很好的做法,而新的APIClient
语言鼓励这样做。但是请记住,使用繁重的纯函数模式重载代码,类别理论方法是错误的想法,因为其他开发人员将阅读并支持您的代码,他们可以对Mantle
以及不可变模型中的这类东西感到沮丧或恐惧。与Swift
相同的是:不要prismatic profunctors
您的代码too much,因为它可能会变得难以理解,特别是对于新手来说。当它可以真正简化您的目标和逻辑时使用它。
所以,ReactiveCocoa
。这是我能给你的最佳建议。
答案 1 :(得分:30)
根据这个问题的目标,我想描述一下我们的架构方法。
我们的一般iOS应用程序架构代表以下模式:Service layers,MVVM,UI Data Binding,Dependency Injection;和Functional Reactive Programming范例。
我们可以将典型的面向消费者的应用程序分割为以下逻辑层:
汇编图层是我们应用程序的引导点。它包含一个Dependency Injection容器和应用程序对象及其依赖项的声明。该层还可能包含应用程序的配置(URL,第三方服务密钥等)。为此,我们使用Typhoon库。
模型图层包含域模型类,验证,映射。我们使用Mantle库来映射我们的模型:它支持将序列化/反序列化为JSON
格式和NSManagedObject
模型。对于我们模型的验证和表单表示,我们使用FXForms和FXModelValidation库。
服务层声明我们用于与外部系统交互的服务,以便发送或接收在我们的域模型中表示的数据。因此,通常我们有服务与服务器API(每个实体),消息服务(如PubNub),存储服务(如Amazon S3)等进行通信。基本上服务包装由SDK提供的对象(例如PubNub SDK)或实现自己的沟通逻辑。对于一般网络,我们使用AFNetworking库。
存储层的目的是在设备上组织本地数据存储。我们使用核心数据或Realm(两者都有利弊,决定使用什么是基于具体的规格)。对于核心数据设置,我们使用MDMCoreData库和一堆类 - 存储 - (类似于服务),它们为每个实体提供对本地存储的访问。对于Realm,我们只使用类似的存储来访问本地存储。
经理层是我们的抽象/包装器所在的地方。
管理员角色可以是:
因此,管理者的角色可以是任何实现应用程序工作所需的特定方面或关注点的逻辑的对象。
我们尽量避免使用Singletons,但如果需要,这层就是他们居住的地方。
协调员图层提供依赖于来自其他图层(服务,存储,模型)的对象的对象,以便将他们的逻辑组合成某个模块所需的一系列工作(功能,屏幕,用户故事)或用户体验)。它通常链接异步操作,并知道如何对其成功和失败案例做出反应。作为示例,您可以设想消息传递功能和相应的MessagingCoordinator
对象。处理发送消息操作可能如下所示:
在上述每个步骤中,相应地处理错误。
UI图层由以下子图层组成:
为了避免使用Massive View控制器,我们使用MVVM模式并实现ViewModel中UI表示所需的逻辑。 ViewModel通常将协调器和管理器作为依赖项。 ViewControllers使用的ViewModels和某些类型的视图(例如表格视图单元格)。 ViewControllers和ViewModels之间的粘合剂是数据绑定和命令模式。为了能够使用该胶水,我们使用ReactiveCocoa库。
我们还使用ReactiveCocoa及其RACSignal
概念作为接口,并返回所有协调器,服务和存储方法的值类型。这允许我们链接操作,并行或串行运行它们,以及ReactiveCocoa提供的许多其他有用的东西。
我们尝试以声明方式实现我们的UI行为。数据绑定和自动布局有助于实现这一目标。
基础架构层包含应用程序工作所需的所有帮助程序,扩展程序和实用程序。
这种方法适用于我们以及我们通常构建的那些类型的应用程序。但你应该明白,这只是一种主观的方法,应该适应/改变具体团队的目的。
希望这会对你有帮助!
此外,您可以在此博文iOS Development as a Service
中找到有关iOS开发过程的更多信息答案 2 :(得分:18)
由于所有iOS应用都不同,我认为这里有不同的方法可以考虑,但我通常会这样:
创建一个中央管理器(单例)类来处理所有API请求(通常命名为APICommunicator),每个实例方法都是一个API调用。还有一种中心(非公开)方法:
-
(RACSignal *)sendGetToServerToSubPath:(NSString *)path withParameters:(NSDictionary *)params;
为了记录,我使用了两个主要的库/框架,ReactiveCocoa和AFNetworking。 ReactiveCocoa可以完美地处理异步网络响应,您可以这样做(sendNext:,sendError:等)。
此方法调用API,获取结果并通过RAC在&#39; raw&#39;格式(如NSArray AFNetworking返回的内容)。
然后调用上述方法的getStuffList:
方法订阅它的信号,将原始数据解析为对象(使用Motis之类的东西)并将对象逐个发送给调用者({{1}并且类似的方法也返回控制器可以订阅的信号。
订阅控制器通过getStuffList:
块接收对象并处理它们。
我在不同的应用程序中尝试了很多方法,但是这个方法效果最好所以我最近在一些应用程序中使用它,它适用于小型和大型项目,并且如果它很容易扩展和维护,如果需要修改的东西。
希望这会有所帮助,我希望听到别人的声音。关于我的方法的意见,也许其他人认为这可能会有所改善。
答案 3 :(得分: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实现的一些示例:
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.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上的某个地方制作一个通用的解决方案。
答案 4 :(得分:7)
在我看来,所有软件架构都是由需求驱动的。如果这是出于学习或个人目的,那么决定主要目标并使其具有驱动架构。如果这是一份招聘工作,那么业务需求是至关重要的。诀窍是不要让闪亮的东西分散你对真实需求的注意力。我觉得这很难做到。在这个行业中总会出现新的闪亮的东西,其中很多都没用,但你不能总是预先告诉它。如果可以的话,专注于需要并愿意放弃糟糕的选择。
例如,我最近为本地商家制作了照片共享应用的快速原型。由于业务需求是做一些快速而又脏的事情,因此架构最终成为一些iOS代码,用于弹出相机和一些连接到发送按钮的网络代码,将按钮上传到S3存储并写入SimpleDB域。代码很简单,成本最低,客户端可以使用REST调用通过Web访问可扩展的照片集。廉价和愚蠢,该应用程序有很多缺陷,偶尔会锁定用户界面,但为原型做更多工作将是一种浪费,它允许他们部署到他们的员工,轻松生成数千个测试图像,没有性能或可扩展性关注。蹩脚的建筑,但它完全符合需求和成本。
另一个项目涉及实施本地安全数据库,当网络可用时,该数据库在后台与公司系统同步。我创建了一个使用RestKit的后台同步器,因为它似乎拥有我需要的一切。但我必须为RestKit编写这么多自定义代码来处理特殊的JSON,我可以通过将自己的JSON写入CoreData转换来更快地完成它。然而,客户希望将这个应用程序带入内部,我觉得RestKit将类似于他们在其他平台上使用的框架。我等着看这是不是一个好的决定。
同样,对我来说问题是关注需求并确定架构。我试着避免使用第三方软件包,因为它们带来的成本只有在应用程序进入该领域一段时间后才会出现。我尽量避免制作类层次结构,因为它们很少得到回报。如果我能在合理的时间内写一些东西而不是采用不完美的包装,那么我就这样做。我的代码结构良好,可以进行调试并进行适当的评论,但第三方软件包很少。话虽如此,我发现AF网络太有用了,无法忽视,结构良好,评论很好,维护得很好,而且我经常使用它! RestKit涵盖了很多常见的情况,但我觉得我在使用它的时候已经陷入困境,而且我遇到的大多数数据源都充满了怪癖和问题,这些问题最好用自定义代码处理。在我的最后几个应用程序中,我只使用内置的JSON转换器并编写一些实用程序方法。
我一直使用的一种模式是从主线程中获取网络调用。我完成的最后4-5个应用程序使用dispatch_source_create设置后台计时器任务,该任务经常醒来并根据需要执行网络任务。您需要执行一些线程安全工作,并确保将UI修改代码发送到主线程。它还有助于以用户不会感到负担或延迟的方式进行入职/初始化。到目前为止,这一直很好。我建议调查这些事情。
最后,我认为随着我们的工作越来越多,随着操作系统的发展,我们倾向于开发更好的解决方案。我花了好几年的时间来克服我的信念,即我必须遵循其他人声称是强制性的模式和设计。如果我的工作环境是当地宗教的一部分,咳咳,我指的是部门最好的工程实践,那么我就按照习俗来写信,这就是他们付给我的。但我很少发现遵循较旧的设计和模式是最佳解决方案。我总是试图通过业务需求的棱镜来看待解决方案,并构建与之匹配的架构,并尽可能保持简单。当我觉得那里还不够,但一切正常时,那我就走在了正确的轨道上。
答案 5 :(得分: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子类并将其添加到队列中。
答案 6 :(得分:3)
我们根据具体情况采用一些方法。对于大多数事情,AFNetworking是最简单和最强大的方法,您可以设置标题,上传多部分数据,使用GET,POST,PUT&amp;删除并为UIKit提供了许多其他类别,例如,您可以从网址设置图像。在一个有很多调用的复杂应用程序中,我们有时会将它抽象为我们自己的方便方法,如下所示:
-(void)makeRequestToUrl:(NSURL *)url withParameters:(NSDictionary *)parameters success:(void (^)(id responseObject))success failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure;
在某些情况下,AFNetworking不合适,例如您在创建框架或其他库组件的位置,因为AFNetworking可能已经在另一个代码库中。在这种情况下,如果要进行单个调用或将其抽象为请求/响应类,则可以使用内联的NSMutableURLRequest。
答案 7 :(得分:1)
我在设计应用程序时避免使用单例。它们是很多人的典型代表,但我认为你可以在其他地方找到更优雅的解决方案。通常,我所做的是在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];
}
为什么在可以通过类别扩展公共基类的功能时添加额外的辅助类?
如果您对我的解决方案有更详细的信息感兴趣请告诉我。我很高兴分享。
答案 8 :(得分:0)
答案 9 :(得分:0)
从纯粹的课堂设计角度来看,你通常会有这样的东西:
数据模型类 - 这实际上取决于您正在处理的真实不同实体的数量,以及它们之间的关联方式。
例如,如果您有一系列要以四种不同的表示形式显示的项目(列表,图表,图形等),您将拥有一个用于项目列表的数据模型类,还有一个用于项目的数据模型类。物品类的列表将由四个视图控制器共享 - 标签栏控制器或导航控制器的所有子项。
数据模型类不仅可以显示数据,还可以序列化它们,其中每个类都可以通过JSON / XML / CSV(或其他任何)导出方法公开自己的序列化格式。
了解您还需要直接映射到REST API端点的 API请求构建器类,这一点非常重要。假设您有一个记录用户的API - 因此您的Login API构建器类将为登录api创建POST JSON有效负载。在另一个示例中,用于目录项API列表的API请求构建器类将为相应的api创建GET查询字符串并触发REST GET查询。
这些API请求构建器类通常会从视图控制器接收数据,并将相同的数据传递回视图控制器以进行UI更新/其他操作。然后,视图控制器将决定如何使用该数据更新数据模型对象。
最后,the heart of the REST client - API data fetcher class无视您的应用所提出的各种API请求。这个类很可能是一个单身人士,但正如其他人指出的那样,它不一定是单身人士。
请注意,该链接只是一个典型的实现,并没有考虑会话,cookie等方案,但它足以让您不使用任何第三方框架。
答案 10 :(得分:0)
这个问题已经有很多优秀和广泛的答案,但我觉得我必须提到它,因为没有其他人有。
斯威夫特的Alamofire。 https://github.com/Alamofire/Alamofire
它是由与AFNetworking相同的人创建的,但更直接地设计了Swift。
答案 11 :(得分: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()
}
}
您将在这里找到GitHub MVVM architecture with rest API Swift Project