• swift moya和ObjectMapper

    2018-05-05 00:47:13
    Moya 使用moya主要是因为网上说这是一个比较推荐的swift开源项目,当一开始学习时看见使用说明就有点扭头要走的冲动,本来一个简单的客户端http request代码分成了好多小块来处理,不过看在有名气的份上还是选择...

    Moya


    使用moya主要是因为网上说这是一个比较推荐的swift开源项目,当一开始学习时看见使用说明就有点扭头要走的冲动,本来一个简单的客户端http request代码分成了好多小块来处理,不过看在有名气的份上还是选择用它。稍试用了一下发现这其实就像一个web服务器框架,按它的规则往里面填空就可以了,只不过我们平常很少在http客户端使用类似的框架。

    另外吐槽一下rxswift,这是因为moya也支持rxswift才想起来的。说实话虽然对于rxswift不怎么了解但是崇尚简单的我觉得学习rxswift就是多此一举,本来连swift我都觉得有点繁琐,没事还搞这么重的一层框架干嘛,非得在一种新语言里搞另一种编程范式,真是吃饱了撑的。

    Swift - 网络抽象层库Moya的使用详解1(安装配置、基本用法)
    Swift - 网络抽象层库Moya的使用详解2(请求参数说明)
    Swift - 网络抽象层库Moya的使用详解3(请求成功、失败的结果处理)
    Swift - 网络抽象层库Moya的使用详解4(单文件上传:文件流方式)
    Swift - 网络抽象层库Moya的使用详解5(多文件上传:MultipartFormData方式)
    Swift - 网络抽象层库Moya的使用详解6(文件下载、资源下载器)
    Swift - 网络抽象层库Moya的使用详解7(多个target使用同一个Provider)
    Swift - 网络抽象层库Moya的使用详解8(创建自定义插件)

    ObjectMapper


    Swift - 使用ObjectMapper实现模型转换1(JSON与Model的相互转换)
    Swift - 使用ObjectMapper实现模型转换2(StaticMappable协议)
    Swift - 使用ObjectMapper实现模型转换3(高级用法)
    Swift - 使用ObjectMapper实现模型转换4(与Alamofire结合使用)

    展开全文
  • 具体的pod用法在这里就不做详解了,如有不懂可以查阅 CocoaPods使用 pod 'Alamofire' pod 'Moya/RxSwift' pod 'HandyJSON', '~> 5.0.0-beta.1' 创建文件 APIManager // // APIManager.swift // ...

    项目Demo地址

    打造swift网络框架

    准备工作

    • 使用CocoaPods工具Pod需要使用的相关框架

    具体的pod用法在这里就不做详解了,如有不懂可以查阅 CocoaPods使用

        pod 'Alamofire' 
        pod 'Moya/RxSwift'
        pod 'HandyJSON', '~> 5.0.0-beta.1'
    

    创建文件

    • APIManager
        //
        //  APIManager.swift
        //  SwiftNetWorkHelper
        //
        //  Created by 罗德良 on 2019/4/10.
        //  Copyright © 2019 swagTeam. All rights reserved.
        //
        
        import Foundation
        import Moya
        
        enum APIManager {
            case testApi
            case testApiPara(para1:String,para2:String)
            case testApiDict(Dict:[String:Any])
            case getNbaInfo(getKey:String)
        }
        
        extension APIManager:TargetType {
            var baseURL: URL {
                return URL.init(string: "http://op.juhe.cn/onebox/basketball/")!
            }
            
            var path: String {
                switch self {
                case .testApi:
                    return "nba"
                case .testApiPara(let para1, let para2):
                    return "nba"
                case .testApiDict:
                    return "nba"
                case .getNbaInfo:
                    return "nba"
                }
            }
            
            var method: Moya.Method {
                switch self {
                case .testApi:
                    return .get
                default:
                    return .post
                }
            }
            
            var sampleData: Data {
                return "".data(using: String.Encoding.utf8)!
            }
            
            var task: Task {
                switch self {
                case .testApi:
                    return .requestPlain
                case let .testApiPara(para1, _):
                    return .requestParameters(parameters: ["key" : para1], encoding: URLEncoding.default)
                case .testApiDict(let dict):
                    return .requestParameters(parameters: dict, encoding: JSONEncoding.default)
                case .getNbaInfo(let getKey):
                    return .requestParameters(parameters: ["key" : getKey], encoding: URLEncoding.default)
                }
            }
            
            var headers: [String : String]? {
                return ["Content-Type":"application/x-www-form-urlencoded"]
            }
        }
      
    • RxHandyJSON
        //
        //  RxHandyJSON.swift
        //  SwiftNetWorkHelper
        //
        //  Created by 罗德良 on 2019/4/10.
        //  Copyright © 2019 swagTeam. All rights reserved.
        //
      
        import Foundation
        import RxSwift
        import HandyJSON
        import Moya
        
        enum DCUError : Swift.Error {
            case ParseJSONError
            case RequestFailed
            case NoResponse
            case UnexpectedResult(resultCode: Int?,resultMsg:String?)
        }
      
        enum RequestStatus: Int {
            case requestSuccess = 200
            case requestError
        }
      
        fileprivate let RESULT_CODE = "code"
        fileprivate let RESULT_MSG = "reason"
        fileprivate let RESULT_DATA = "result"
      
        public extension Observable {
            func mapResponseToObject<T: HandyJSON>(type: T.Type) -> Observable<T> {
                return map { response in
                    guard let response = response as? Moya.Response
                        else {
                            throw DCUError.NoResponse
                    }
                    guard ((200...209) ~= response.statusCode) else {
                        throw DCUError.RequestFailed
                    }
                    
                    let jsonData = try response.mapJSON() as! [String : Any]
                    if let code = jsonData[RESULT_MSG] as? String {
                        if code == "查询成功" {
                            if let model = JSONDeserializer<T>.deserializeFrom(dict: jsonData){
                                return model
                            }
                        }
                    }
        }
      
    • TestModel
        //
        //  TestModel.swift
        //  SwiftNetWorkHelper
        //
        //  Created by 罗德良 on 2019/4/10.
        //  Copyright © 2019 swagTeam. All rights reserved.
        //
        
        import HandyJSON
        
        struct TestModel: HandyJSON {
            var reason :String = ""
            var result :String = ""
        }
      

    实现效果

    let rxProvider = MoyaProvider<APIManager>()
            rxProvider.rx.request(.getNbaInfo(getKey: "537f7b3121a797c8d18f4c0523f3c124")).asObservable().mapResponseToObject(type: TestModel.self)
                .subscribe { (test) in
                    print(test)
                    let model = test.element
                    print(model?.reason ?? String.self)
            }
            .disposed(by: disposeBag)
    

    总结

    Demo链接

    • APIManager作为接口内容配置,通过接口地址以及接口数据的组建
    • RxHandyJSON作为由于Moya对HandyJson没有扩展,自己实现的扩展,该文件可以根据后台接口定义做调整
    • TestModel作为基础数据格式,该处可以创建一个BaseModel,根据后台返回数据进行调整,会有更好的扩展性

    Roder, 我的博客

    展开全文
  • Moya源码解析

    2016-07-04 08:20:42
    Moya源码解析Moya是一个高度抽象的网络库,他的理念是让你不用关心网络请求的底层的实现细节,只用定义你关心的业务。且Moya采用桥接和组合来进行封装(默认桥接了Alamofire),使得Moya非常好扩展,让你不用修改Moya...

    Moya是一个高度抽象的网络库,他的理念是让你不用关心网络请求的底层的实现细节,只用定义你关心的业务。且Moya采用桥接和组合来进行封装(默认桥接了Alamofire),使得Moya非常好扩展,让你不用修改Moya源码就可以轻易定制。官方给出几个Moya主要优点:

    • 编译时检查API endpoint权限
    • 让你使用枚举定义各种不同Target, endpoints
    • 把stubs当做一等公民对待,因此测试超级简单。

    Target

    开始Moya之旅的第一步便是,建立一个Enum的Target,这个Target便是你网络请求相关行为的定义。Target必须实现TargetType协议。

    public protocol TargetType {
        var baseURL: NSURL { get }
        var path: String { get }
        var method: Moya.Method { get }
        var parameters: [String: AnyObject]? { get }
        var sampleData: NSData { get }
    } 

    例如有一个AccountAPI模块,模块实现注册登录的功能。所以第一件事情,我们需要定义一个Target

    enum AccountAPI {
        case Login(userName: String, passwd: String)
        case Register(userName: String, passwd: String)
    }
    
    extension AccountAPI: TargetType {
        var baseURL: NSURL {
            return NSURL(string: "https://www.myapp.com")!
        }
    
        var path: String {
            switch self {
            case .Login:
                return "/login"
            case .Register:
                return "/register"
            }
        }
    
        var method: Moya.Method {
            return .GET
        }
    
        var parameters: [String: AnyObject]? {
            switch self {
            case .Login:
                return nil
            case .Register(let userName, let passwd):
                return ["username": userName, "password": passwd]
            }
        }
    
        var sampleData: NSData {
            switch self {
            case .Login:
                return "{'code': 1,6'Token':'123455'}".dataUsingEncoding(NSUTF8StringEncoding)!
            case .Register(let userName, let passwd):
                return "找不到数据"
            }
        }
    }

    主要是实现了TargetType协议,里面的网址和内容,是随便写的,可能不make sence(不合理), 但 仅仅是做一个例子而已。

    Providers

    Providers是Moya中的核心,Moya中所有的API请求都是通过Provider来发起的。因此大多数时候,你的代码请求像这样:

    let provider = MoyaProvider<AccountAPI>()
    provider.request(.Login) { result in
        // `result` is either .Success(response) or .Failure(error)
    }
    

    我们初始化了一个AccountAPI的Provider,并且调用了Login请求。怎么样?干净简单吧!

    从Provider的构造函数说起

    Provider真正做的事情可以用一个流来表示:Target -> Endpoint -> Request 。在这个例子中,它将AccountAPI转换成Endpoint, 再将其转换成为NSRURLRequest。最后将这个NSRURLRequest交给Alamofire去进行网络请求。

    我们从Provider的构造函数开始切入,一步一步地扒开它。

    //Moya.swift
    
    public init(endpointClosure: EndpointClosure = MoyaProvider.DefaultEndpointMapping,
            requestClosure: RequestClosure = MoyaProvider.DefaultRequestMapping,   
            stubClosure: StubClosure = MoyaProvider.NeverStub,
            manager: Manager = MoyaProvider<Target>.DefaultAlamofireManager(),
            plugins: [PluginType] = []) 
    1. 首先我们发现的是3个Closure:endpointClosure、requestClosure、stubClosure。这3个Closure是让我们定制请求和进行测试时用的。非常有用,后面细说。

    2. 然后是一个Manager,Manager是真正用来网络请求的类,Moya自己并不提供Manager类,Moya只是对其他网络请求类进行了简单的桥接。这么做是为了让调用方可以轻易地定制、更换网络请求的库。比如你不想用Alamofire,可以十分简单的换成其他库

    3. 最后是一个类型为PluginType的数组。Moya提供了一个插件机制,使我们可以建立自己的插件类来做一些额外的事情。比如写Log,显示“菊花”等。抽离出Plugin层的目的,就是让Provider职责单一,满足开闭原则。把和自己网络无关的行为抽离。避免各种业务揉在一起不利于扩展。

    先来看看第一个EndpointClosure

    EndpointClosure

    //Moya.swift
    
    public typealias EndpointClosure = Target -> Endpoint<Target>

    EndpointClosure这个闭包,输入是一个Target,返回Endpoint。这就是我们前面说的Target -> Endpoint的转换,那么Endpoint是个什么鬼?
    Endpoint 是Moya最终进行网络请求前的一种数据结构,它保存了这些数据:

    • URL
    • HTTP请求方式 (GET, POST, etc).
    • 本次请求的参数
    • 参数的编码方式 (URL, JSON, custom, etc).
    • stub数据的 response(测试用的)
    //Endpoint.swift
    
    public class Endpoint<Target> {
        public typealias SampleResponseClosure = () -> EndpointSampleResponse
    
        public let URL: String
        public let method: Moya.Method
        public let sampleResponseClosure: SampleResponseClosure
        public let parameters: [String: AnyObject]?
        public let parameterEncoding: Moya.ParameterEncoding
    
          ...
      }

    Moya提供一个默认EndpointClosure的函数,来实现这个Target到Endpoint的转换:

    //Moya.swift
    
    public final class func DefaultEndpointMapping(target: Target) -> Endpoint<Target> {
         let url = target.baseURL.URLByAppendingPathComponent(target.path).absoluteString
         return Endpoint(URL: url, sampleResponseClosure: {.NetworkResponse(200, target.sampleData)}, method: target.method, parameters: target.parameters)
     }

    上面的代码只是单纯地创建并返回一个Endpoint实例。然而在很多时候,我们需要自定义这个闭包来做更多额外的事情。后面在stub小节,你会看到,我们用stub模拟API请求失败的场景,给客户端返回一个非200的状态码。为了实现这个功能,在这个闭包里处理相关的逻辑,再合适不过了!或者说这个闭包就是让我们根据业务需求定制网络请求的。

    RequestClosure

    //Moya.swift
    
    public typealias RequestClosure = (Endpoint<Target>, NSURLRequest -> Void) -> Void

    RequestClosure这个闭包就是实现将Endpoint -> NSURLRequest,Moya也提供了一个默认实现:

    //Moya.swift
    
    public final class func DefaultRequestMapping(endpoint: Endpoint<Target>, closure: NSURLRequest -> Void) {
          return closure(endpoint.urlRequest)
    }

    默认实现也只是简单地调用endpoint.urlRequest取得一个NSURLRequest实例。然后调用了closure。然而,你可以在这里修改这个请求Request, 事实上这也是Moya给你的最后的机会。举个例子, 你想禁用所有的cookie,并且设置超时时间等。那么你可以实现这样的闭包:

    let requestClosure = { (endpoint: Endpoint<GitHub>, done: NSURLRequest -> Void) in
        //可以在这里修改request
        let request: NSMutableURLRequest = endpoint.urlRequest.mutableCopy() as NSMutableURLRequest
        request.HTTPShouldHandleCookies = false
        request.timeoutInterval = 20 
    
        done(request)
    }
    
    provider = MoyaProvider(requestClosure: requestClosure)
    

    从上面可以清晰地看出,EndpointClosureRequestClosure 实现了 Target -> Endpoint -> NSRequest的转换流

    StubClosure

    //Moya.swift
    
    public typealias StubClosure = Target -> Moya.StubBehavior

    StubClosure这个闭包比较简单,返回一个StubBehavior的枚举值。它就是让你告诉Moya你是否使用Stub返回数据或者怎样使用Stub返回数据

    //Moya.swift
    
    public enum StubBehavior {
        case Never          //不使用Stub返回数据
        case Immediate      //立即使用Stub返回数据
        case Delayed(seconds: NSTimeInterval) //一段时间间隔后使用Stub返回的数据
    }

    Never表明不使用Stub来返回模拟的网络数据, Immediate表示马上返回Stub的数据, Delayed是在几秒后返回。Moya默认是不使用Stub来测试。

    在Target那一节我们定义了一个AccountAPI, API中我们实现了接口sampleData, 这个属性是返回Stub数据的。

    extension AccountAPI: TargetType {
        ...
        var sampleData: NSData {
            switch self {
            case .Login:
                return "{'code': 1,6'Token':'123455'}".dataUsingEncoding(NSUTF8StringEncoding)!
            case .Register(let userName, let passwd):
                return "找不到数据"
            }
        }
    }
    
    let endPointAction = { (target: TargetType) -> Endpoint<AccountAPI> in
        let url = target.baseURL.URLByAppendingPathComponent(target.path).absoluteString
    
        switch target {
        case .Login:
            return Endpoint(URL: url, sampleResponseClosure: {.NetworkResponse(200, target.sampleData)}, method: target.method, parameters: target.parameters)
        case .Register:
            return Endpoint(URL: url, sampleResponseClosure: {.NetworkResponse(404, target.sampleData)}, method: target.method, parameters: target.parameters)
        }
    }
    
    let stubAction: (type: AccountAPI) -> Moya.StubBehavior  = { type in
        switch type {
        case .Login:
            return Moya.StubBehavior.Immediate
        case .Register:
            return Moya.StubBehavior.Delayed(seconds: 3)
        }
    }
    let loginAPIProvider = MoyaProvider<AccountAPI>(
        endpointClosure: endPointAction,
        stubClosure: stubAction
    )
    
    self.netProvider = loginAPIProvider
    loginAPIProvider.request(AccountAPI.Login(userName: "user", passwd: "123456")) { (result) in
        switch result {
        case .Success(let respones) :
            print(respones)
    
        case .Failure(_) :
            print("We got an error")
        }
        print(result)
    }
    

    就这样我们就实现了一个Stub! Login和Register都使用了Stub返回的数据。

    注意:Moya中Provider对象在销毁的时候会去Cancel网络请求。为了得到正确的结果,你必须保证在网络请求的时候你的Provider不会被释放。否者你会得到下面的错误 “But don’t forget to keep a reference for it in property. If it gets deallocated you’ll see -999 “cancelled” error on response” 。通常为了避免这种情况,你可以将Provider实例设置为类成员变量,或者shared实例

    Moya中Stub的实现

    大多iOS的Http的Stub框架本质都是实现一个HTTP网络请求的代理类,去Hook系统Http请求。 如OHHTTPStub就是这么做的。在iOS中,HTTP代理类需要继承NSURLProtocol类,重载一些父类的方法,然后将这个代理类注册到系统中去。

    class MyHttpProxy : NSURLProtocol {
        //重载一些父类的方法
         override class func canInitWithRequest(request: NSURLRequest) -> Bool {
            return true
        }
    
        override class func canonicalRequestForRequest(request: NSURLRequest) -> NSURLRequest {
            return super.canonicalRequestForRequest(request)
        }
    
        ....
    }
    
    //注册
    NSURLProtocol.registerClass(MyHttpProxy.self) 
    

    之后我们APP中所有的网络请求,都会去经过我们MyHttpProxy的代理类。
    然而Moya的Stub不是这样的,Moya的Stub的实现原理也超级无敌简单!它不是系统级别的,非入侵式的。它只是简单的加了一个判断而已!还是在Moya的Request方法里面

    //Moya.swift
    
    public func request(target: Target, queue:dispatch_queue_t?, completion: Moya.Completion) -> Cancellable {
            let endpoint = self.endpoint(target)
            let stubBehavior = self.stubClosure(target)
            var cancellableToken = CancellableWrapper()
    
            let performNetworking = { (request: NSURLRequest) in
                if cancellableToken.isCancelled { return }
    
                switch stubBehavior {
                case .Never:
                    cancellableToken.innerCancellable = self.sendRequest(target, request: request, queue: queue, completion: completion)
                default:
                    cancellableToken.innerCancellable = self.stubRequest(target, request: request, completion: completion, endpoint: endpoint, stubBehavior: stubBehavior)
                }
            }
    
            requestClosure(endpoint, performNetworking)
    
            return cancellableToken
        }

    Moya先调用我们在构造函数中传入的stubClosure闭包,如果stubBehavior是Never就真正的发起网络请求,否
    者就调用self.stubRequest

    //Moya.swift
    
    internal func stubRequest(target: Target, request: NSURLRequest, completion: Moya.Completion, endpoint: Endpoint<Target>, stubBehavior: Moya.StubBehavior) -> CancellableToken {
            ...
            let stub: () -> () = createStubFunction(cancellableToken, forTarget: target, withCompletion: completion, endpoint: endpoint, plugins: plugins)
            switch stubBehavior {
            case .Immediate:
                stub()
            case .Delayed(let delay):
                let killTimeOffset = Int64(CDouble(delay) * CDouble(NSEC_PER_SEC))
                let killTime = dispatch_time(DISPATCH_TIME_NOW, killTimeOffset)
                dispatch_after(killTime, dispatch_get_main_queue()) {
                    stub()
                }
            case .Never:
                fatalError("Method called to stub request when stubbing is disabled.")
            }
    
            ...
    }

    如果Immediate,就马上调用stub返回,是Delayed的话就Dispatch after延迟调用。

    Manager

    我们知道,Moya并不是一个网络请求的三方库,它只是一个抽象的网络层。它对其他网络库的进行了桥接,真正进行网络请求是别人的网络库(比如默认的Alamofire.Manager)
    为了达到这个目的Moya做了几件事情:

    首先抽象了一个RequestType协议,利用这个协议将Alamofire隐藏了起来,让Provider类依赖于这个协议,而不是具体细节。

    //Plugin.swift
    
    public protocol RequestType {
        var request: NSURLRequest? { get }
    
        func authenticate(user user: String, password: String, persistence: NSURLCredentialPersistence) -> Self
    
        func authenticate(usingCredential credential: NSURLCredential) -> Self
    }

    然后让Moya.Manager == Alamofire.Manager,并且让Alamofire.Manager也实现RequestType协议

    Moya+Alamofire.swift
    
    public typealias Manager = Alamofire.Manager
    
    /// Choice of parameter encoding.
    public typealias ParameterEncoding = Alamofire.ParameterEncoding
    
    //让Alamofire.Manager也实现 RequestType协议
    extension Request: RequestType { }

    上面几步,就完成了Alamofire的封装、桥接。正因为桥接封装了Alamofire, 因此Moya的request,最终一定会调用Alamofire的request。简单的跟踪下Moya的Request方法就可以发现sendRequest调用了Alamofire。

    //Moya.swift
    
    func sendRequest(target: Target, request: NSURLRequest, queue: dispatch_queue_t?, completion: Moya.Completion) -> CancellableToken {
        //调用Alamofire发起网络请求
        let alamoRequest = manager.request(request)
            ...
    }
    

    如果你想自定义你自己的Manager, 你可以传入你自己的Manager到Privoder。之后所有的请求都会经过你的这个Manager

    let policies: [String: ServerTrustPolicy] = [
        "example.com": .PinPublicKeys(
            publicKeys: ServerTrustPolicy.publicKeysInBundle(),
            validateCertificateChain: true,
            validateHost: true
        )
    ]
    
    let manager = Manager(
        configuration: NSURLSessionConfiguration.defaultSessionConfiguration(),
        serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies)
    )
    
    let provider = MoyaProvider<MyTarget>(manager: manager)

    Plugin

    Moya提供还提供插件机制,你可以自定义各种插件,所有插件必须满足PluginType协议

    //Plugin.swift
    
    public protocol PluginType {
        /// Called immediately before a request is sent over the network (or stubbed).
        func willSendRequest(request: RequestType, target: TargetType)
    
        // Called after a response has been received, but before the MoyaProvider has invoked its completion handler.
        func didReceiveResponse(result: Result<Moya.Response, Moya.Error>, target: TargetType)
    }

    协议里只有两个方法,willSendRequest和didReceiveResponse。在进行网络请求之前和收到请求后,Moya会遍历所有的插件。分别去调用插件各自的willSendRequest和didReceiveResponse方法。

    个人觉得这个插件更像是一个网络回调的Delegate,只是取了一个高大上的名字而已。不过将网络回调抽取出来确实能更好地将无关业务隔离,让Privoder更加专心的做自己的事情。而且以后也非常好扩展。

    Moya默认提供了三个插件:

    • Authentication插件 (CredentialsPlugin.swift)。 HTTP认证的插件。
    • Logging插件(NetworkLoggerPlugin.swift)。在调试是,输入网络请求的调试信息到控制台
    • Network Activity Indicator插件(NetworkActivityPlugin.swift)。可以用这个插件来显示网络菊花

    Network Activity Indicator插件用法示例,在网络进行请求开始请求时添加一个Spinner, 请求结束隐藏Spinner。这里用的是SwiftSpinner

    let spinerPlugin = NetworkActivityPlugin { state in
        if state == .Began {
            SwiftSpinner.show("Connecting...")
        } else {
            SwiftSpinner.show("request finish...")
            SwiftSpinner.hide()
        }
    
    let loginAPIProvider = MoyaProvider<AccountAPI>(
        plugins: [spinerPlugin]
    )
    
    loginAPIProvider.request(.Login) { _ in }
    

    插件实现代码

    插件的源码实现也超级简单。在进行网络请求之前和收到请求后,遍历所有的插件,调用其相关的接口。只是要分别处理下Stub和真正进行网络请求的两种情况

    //Moya.swift
    
    func sendRequest(target: Target, request: NSURLRequest, queue: dispatch_queue_t?, completion: Moya.Completion) -> CancellableToken {
            let alamoRequest = manager.request(request)
            let plugins = self.plugins
    
            // 遍历插件,通知开始请求
            plugins.forEach { $0.willSendRequest(alamoRequest, target: target) }
    
            // Perform the actual request
            alamoRequest.response(queue: queue) { (_, response: NSHTTPURLResponse?, data: NSData?, error: NSError?) -> () in
                let result = convertResponseToResult(response, data: data, error: error)
                // 遍历插件,通知收到请求
                plugins.forEach { $0.didReceiveResponse(result, target: target) }
                completion(result: result)
            }
    
            alamoRequest.resume()
    
            return CancellableToken(request: alamoRequest)
        }
    
    //在测试时,Stub分支的也要,遍历调用一次插件
    
    internal final func createStubFunction(token: CancellableToken, forTarget target: Target, withCompletion completion: Moya.Completion, endpoint: Endpoint<Target>, plugins: [PluginType]) -> (() -> ()) {
            return {
                if (token.canceled) {
                    let error = Moya.Error.Underlying(NSError(domain: NSURLErrorDomain, code: NSURLErrorCancelled, userInfo: nil))
                    //调用插件
                    plugins.forEach { $0.didReceiveResponse(.Failure(error), target: target) }
                    completion(result: .Failure(error))
                    return
                }
    
                switch endpoint.sampleResponseClosure() {
                case .NetworkResponse(let statusCode, let data):
                    let response = Moya.Response(statusCode: statusCode, data: data, response: nil)
                    //成功情况,调用插件
                    plugins.forEach { $0.didReceiveResponse(.Success(response), target: target) }
                    completion(result: .Success(response))
                case .NetworkError(let error):
                    let error = Moya.Error.Underlying(error)
                    //失败情况,调用插件
                    plugins.forEach { $0.didReceiveResponse(.Failure(error), target: target) }
                    completion(result: .Failure(error))
                }
            }
        }
    

    总结


    总的来说Moya的实现比较简单,但是基于作者这种桥接、封装的思路,使得Moya扩展十分灵活,所以Moya有各种Provider, 能和RxSwift, RAC等等轻松的结合。 而Moya用起来也非常的干净。你不用关心Request具体实现。只用专注于你自己的Target设计就行。再加上Moya的Stub特性,的确使得它十分易于测试。

    自己的思考


    成也萧何败也萧何。然而我自己的感受,Moya让我们把所有的业务都放到Target中去,也会导致另外一些问题:
    (以下仅是个人观点,仅供参考)

    1. 枚举无法重载,代码未必简洁
      比如,现在要添加一个新接口,还是要求实现Login功能,除了支持已有的用户名/密码登录,还要支持指纹登录。那么我们想定义可能想这样:Login(fingerPrint: String)。这两种登录情况实际上只是参数不一样。但在因为枚举中不能重载,所以为了添加这个case,我们不得不重新取一个名字,而不能利用函数重载。

      enum AccountAPI {
      case Login(userName: String, passwd: String)
      case Register(userName: String, passwd: String)
      //case Login(fingerPrint: String) //error: 不能这样添加错的,不支持重载
      case LoginWithPrint(fingerPrint: String) //正确. 只能改名
      }
      

      我个人觉得这样做,似乎并没有重载简洁。相比修改名字,我更喜欢重载。

    2. Target碎片化,后期维护困难
      随着业务的增加,Target会变得很复杂。TargetType协议它是利用多个属性:method属性、parameters属性等。将一次API请求的实现的分割到多个了函数(属性)中去实现。这就导致实现碎片化了。添加一个API请求,你需要修改几个函数(属性), 改几个switch语句。如果文件很长,修改起来真的很烦,根本不好归类整理。

    3. 不利于多人协作开发
      因为大家每次添加新功能,修改的都是这几个相同的函数(属性),所以非常容易导致文件冲突。

    展开全文
  • RxSwift+Moya之项目实战

    2017-10-08 18:59:23
    RxSwift+Moya之项目实战 RxSwift相关基本介绍和用法可参考: RxSwift的使用详解01 RxSwift的使用详解02 一. 下面将将进行实战项目 1.登录注册功能 输入用户名要大于6个字符,不然密码不能输入 密码必须大于6个...

    RxSwift+Moya之项目实战

    RxSwift相关基本介绍和用法可参考:
    RxSwift的使用详解01
    RxSwift的使用详解02

    一. 下面将将进行实战项目

    • 1.登录注册功能
      • 输入用户名要大于6个字符,不然密码不能输入
      • 密码必须大于6个字符,不然重复密码不能输入
      • 重复密码输入必须和密码一样,不然注册按钮不能点击
      • 根据输入的字符是否合法,按钮动态的改变颜色
    • 2.UITableView和搜索SertchBar的应用
      • searchBar根据输入的字体展示包含该字体的cell列表
      • RxSwift实现tableView列表展示
    • 3.Moya+RxSwift实现网络请求

    二. Demo地址

    下面简单看一下demo的界面

    1. 登录注册

    登录注册

    2. UITableView和SearchBar

    UITableView和SearchBar

    3. UICollectionView和Moya

    UICollectionView和Moya

    三. 项目结构和框架

    1. 结构

    demo是使用的纯MVVM模式,因为RxSwift就是为MVVM而生。不懂MVVM的猿友可参考MVVM模式快速入门

    项目结构

    2. 项目框架

    // Swift三方库
        // Rx
        pod 'RxSwift'  //RxSwift的必备库
        pod 'RxCocoa'  //对 UIKit Foundation 进行 Rx 化
        pod 'RxDataSources'   // 帮助我们优雅的使用tableView的数据源方法
    
        // 网络请求
        pod 'Moya/RxSwift'  // 为RxSwift专用提供,对Alamofire进行封装的一个网络请求库
    
        // 图片处理
        pod 'Kingfisher'  //图片处理库
    
        // 数据解析
        pod 'ObjectMapper'  //json转模型
    
    
    
    // OC库
        // MJRefresh
        pod 'MJRefresh'   //MJ上拉下拉刷新
        pod 'SVProgressHUD'  //HUD
    

    四. 注册界面

    • 这里主要使用了Observable的相关知识,不了解的童鞋可参考RxSwift的使用详解01,了解Observable的操作
    • 注册和登录并没有保存已注册的账号和密码, 故登录功能并不完善,后期会在完善,望知晓
    • 下面将针对注册用户名做简单介绍:

    1. 首先在model里处理输入字符串的语法法则和字符个数是否符合规范

    extension InputValidator {
        //判断字符串是否符合语法法则
        class func isValidEmail(_ email: String) -> Bool {
            let regular = try? NSRegularExpression(pattern: "^\\S+@\\S+\\.\\S+$", options: [])
            if let re = regular {
                let range = NSRange(location: 0, length: email.lengthOfBytes(using: .utf8))
                let result = re.matches(in: email, options: [], range: range)
                return result.count > 0
            }
            return false
        }
    
        //判断密码字符个数>8
        class func isValidPassword(_ password: String) -> Bool {
            return password.characters.count >= 8
        }
    
        //判断用户名
        class func validateUserName(_ username: String) -> Result {
            //判断字符个数是否正确
            if username.characters.count < 6 {
                return Result.failure(message: "输入的字符个数不能少于6个字符")
            }
    
            //账号可用
            return Result.success(message: "账号可用")
        }
    }

    其中Result是一个返回是否成功的枚举值,可传入字符串变量

    enum Result {
        case success(message: String)
        case failure(message: String)
    }

    2. 根据输入的用户名判断该用户名是否可用

        var usernameObserable: Observable<Result>
        var passwordObserable: Observable<Result>
        var repeatPassObserable: Observable<Result>
        var registerBtnObserable: Observable<Bool>
    
    
        init(){
            //检测账号
            usernameObserable = username.asObservable().map({ (username) -> Result in
                return InputValidator.validateUserName(username)
            })
        }    
    
    • 该返回参数Result,控制器将根据该Result是否成功来改变输入框是否是可编辑状态
    • 初始化方法中,我们对传入的序列进行处理和转换成相对应的Result序列

    3. controller逻辑,根据用户名输入改变各控件状态

            //1. 账号判断逻辑
            //1-1. 检测账号
            usernameTextField.rx.text
                .orEmpty // 将String? 类型转为String型
                .bindTo(registerVM.username)
                .addDisposableTo(bag)
    
            //1-2. 根据账号监听提示字体的状态
            registerVM.usernameObserable
                .bindTo(usernameHintLabel.rx.validationResult)
                .addDisposableTo(bag)
    
            //1-3. 根据账号监听密码输入框的状态
            registerVM.usernameObserable
                .bindTo(passwordTextField.rx.enableResult)
                .addDisposableTo(bag)
    
    • 检测输入用户名是否符合规范
    • 根据账号监听提示字体的状态
    • 根据账号监听密码输入框的状态
    • 根据账号监听注册按钮的状态

    五. UITableView和SearchBar

    • 该UITableView展示界面并未涉及网络请求
    • 数据来源plist文件
    • 图片为本地图片,可下载demo,在demo中查找图片
    • 选用自定义UITableViewCell,故cell不做介绍
    • model小编这里也不多做介绍,详情可下载demo看具体代码

    1. viewModel中的代码逻辑

    1-1. 读取plist文件,获取模型数组

    fileprivate func getHeroData() -> [HeroModel]{
        // 1.获取路径
        let path = Bundle.main.path(forResource: "heros.plist", ofType: nil)!
    
        // 2.读取文件内容
        let dictArray = NSArray(contentsOfFile: path) as! [[String : Any]]
    
        // 3.遍历所有的字典并且转成模型对象
        return dictArray.map({ HeroModel(dict: $0) }).reversed()
    }

    1-2. seachBar

        lazy var heroVariable: Variable<[HeroModel]> = {
            return Variable(self.getHeroData())
        }()
    
        var searchText: Observable<String>
        init(searchText: Observable<String>) {
            self.searchText = searchText
    
            self.searchText.subscribe(onNext: { (str: String) in
                let heros = self.getHeroData().filter({ (hero: HeroModel) -> Bool in
                    //过滤
                    if str.isEmpty { return true }
                    //model是否包含搜索字符串
                    return hero.name.contains(str)
                })
                self.heroVariable.value = heros
            }).addDisposableTo(bag)
        }
    
    • 其中heroVariable是一个数组模型的包装箱,在controller内调用使用前需要asObservable或者asDriver解包装;详细用法可参考:RxSwift的使用详解01
    • searchText搜索框输入的关键字,根据该关键字从数组中过滤出所有包含该关键字的model
    • 对heroVariable重新赋值,发出事件

    1-3. RxTableViewController.swift主要代码

    1-3-1. searchBar搜索框,输入字符后间隔0.5秒开始搜索

    var searchText: Observable<String> {
        //输入后间隔0.5秒搜索,在主线程运行
        return searchBar.rx.text.orEmpty.throttle(0.5, scheduler: MainScheduler.instance)
    }
    

    1-3-2. UITableView的设置

        //2.给tableView绑定数据
        //注意: 三个参数:row, model, cell三个顺序不可以搞错, 不需要的可省略 
        heroVM.heroVariable.asDriver().drive(rxTableView.rx.items(cellIdentifier: kCellID, cellType: RxTableViewCell.self)) { (_, hero, cell) in
            cell.heroModel = hero
        }.addDisposableTo(bag)
    
        // 3.监听UITableView的点击
        rxTableView.rx.modelSelected(HeroModel.self).subscribe { (event: Event<HeroModel>) in
            print(event.element?.name ?? "")
        }.addDisposableTo(bag)
    
    • 将viewModel中的heroVariable进行解包装,如果是Driver序列,我们这里不使用bingTo,而是使用的Driver,用法和bingTo一模一样。
    • Deriver的监听一定发生在主线程,所以很适合我们更新UI的操作
    • 如需设置delegate的代理
    rxTableView.rx.setDelegate(self).addDisposableTo(bag)

    然后在实现相应的代理方法即可,如:

    extension RxTableViewController: UITableViewDelegate{
        func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
            return 100
        }
    }

    六. UICollectionView+Moya+ObjectMapper网络请求和数据处理

    • 与上述UITableView不同的是,这部分将以RxDataSources处理数据源
    • model数组以sections组集合处理
    • 结合Moya进行网络请求
    • 使用ObjectMapper进行json数据转模型

    1. 配合ObjectMapper

    这里再介绍一下ObjectMapper

    class AnchorModel: Mappable {
    
        var name = ""    //名字
        var pic51 = ""   //头像
        var pic74 = ""   //大图
        var live = 0
        var push = 0
        var focus = 0    //关注量
    
        required init?(map: Map) {
    
        }
    
        func mapping(map: Map) {
            name  <- map["name"]
            pic51 <- map["pic51"]
            pic74 <- map["pic74"]
            live  <- map["live"]
            push  <- map["push"]
            focus <- map["focus"]
        }
    }
    
    • 使用 ObjectMapper ,需要让自己的 Model 类使用 Mappable 协议,这个协议包括两个方法:
    required init?(map: Map) {}
    
    func mapping(map: Map) {}
    • 在 mapping 方法中,用 <- 操作符来处理和映射你的 JSON数据
    • 详细的 ObjectMapper 教程可以查看它的 Github 主页,我在这里只做简单的介绍。

    2. Moya的使用

    • Moya是基于Alamofire的网络请求库,这里我使用了Moya/Swift,它在Moya的基础上添加了对RxSwift的接口支持。
    • Github上的官方介绍罗列了Moya的一些特点:
      • 编译时检查正确的API端点访问.
      • 使你定义不同端点枚举值对应相应的用途更加明晰.
      • 提高测试地位从而使单元测试更加容易.
    • 接下来我们来说下Moya的使用

    2-1. 创建一个枚举API

    //请求枚举类型
    enum JunNetworkTool {
    
        case getNewList
        case getHomeList(page: Int)
    }

    2-2. 为枚举添加扩展

    • 需遵循协议 TargetType
    • 这个协议的Moya这个库规定的协议,可以单击进入相应的文件进行查看
    • 这个协议内的每一个参数(除了validate可不重写)都必须重写,否则会报错
    //请求参数
    extension JunNetworkTool: TargetType {
    
        //统一基本的url
        var baseURL: URL {
            return (URL(string: "http://qf.56.com/home/v4/moreAnchor.ios"))!
        }
    
        //path字段会追加至baseURL后面
        var path: String {
            return ""
        }
    
        //请求的方式
        var method: Moya.Method {
            return .get
        }
    
        //参数编码方式(这里使用URL的默认方式)
        var parameterEncoding: ParameterEncoding {
            return URLEncoding.default
        }
    
        //用于单元测试
        var sampleData: Data {
            return "getList".data(using: .utf8)!
        }
    
        //将要被执行的任务(请求:request 下载:upload 上传:download)
        var task: Task {
            return .request
        }
    
        //请求参数(会在请求时进行编码)
        var parameters: [String: Any]? {
            switch self {
            case .getHomeList(let index):
                return ["index": index]
            default:
                return ["index": 1]
            }
        }
    
        //是否执行Alamofire验证,默认值为false
        var validate: Bool {
            return false
        }
    }
    

    2-3. 定义一个全局变量用于整个项目的网络请求

    let junNetworkTool = RxMoyaProvider<JunNetworkTool>()

    至此,我们就可以使用这个全局变量来请求数据了

    3. RxDataSources

    • RxDataSources是以section来做为数据结构来传输,这点很重要,比如:在传统的数据源实现的方法中有一个numberOfSection,我们在很多情况下只需要一个section,所以这个方法可实现,也可以不实现,默认返回的就是1,这给我们带来的一个迷惑点:【tableView是由row来组成的】,不知道在坐的各位中有没有是这么想的呢??有的话那从今天开始就要认清楚这一点,【tableView其实是由section组成的】,所以在使用RxDataSources的过程中,即使你的setion只有一个,那你也得返回一个section的数组出去!!!
    • 传统方式适用于简单的数据集,但不处理需要将复杂数据集与多个部分进行绑定的情况,或者在添加/修改/删除项目时需要执行动画时。而使用RxDataSources时,它很容易写
    • 想了解更多关于RxDataSources的用法,请参考其GitHub主页

    3-1. Sections自定义

    • 在我们自定义的Model中创建一个AnchorSection的结构体
    • 并遵循SectionModelType协议,实现相应的协议方法
    //MARK: SectionModel
    struct AnchorSection {
        // items就是rows
        var items: [Item]
    
        // 你也可以这里加你需要的东西,比如 headerView 的 title
    }
    
    extension AnchorSection: SectionModelType {
        // 重定义 Item 的类型为
        typealias Item = AnchorModel
        init(original: AnchorSection, items: [AnchorSection.Item]) {
            self = original
            self.items = items
        }
    }

    4. ViewModel

    4-1. 自定义协议BaseViewModel

    我们知道MVVM思想就是将原本在ViewController的视图显示逻辑、验证逻辑、网络请求等代码存放于ViewModel中,让我们的ViewController瘦身。这些逻辑由ViewModel负责,外界不需要关心,外界只需要结果,ViewModel也只需要将结果给到外界,基于此,我们定义了一个协议

    
    protocol JunViewModelType {
        //associatedtype: 关联类型为协议中的某个类型提供了一个占位名(或者说别名),其代表的实际类型在协议被采纳时才会被指定
        associatedtype Input
        associatedtype Output
    
        //我们通过 transform 方法将input携带的数据进行处理,生成了一个Output
        func transform(input: Input) -> Output
    }
    

    4-2. 自定义用于网络请求的刷新状态

    • 根据枚举值的判断,改变collection的刷新状态
    //刷新的状态
    enum JunRefreshStatus {
        case none
        case beingHeaderRefresh
        case endHeaderRefresh
        case beingFooterRefresh
        case endFooterRefresh
        case noMoreData
    }
    

    4-3. 自定义用于继承的BaseViewModel

    • 定义请求数据的页数index
    • 定义input和output的结构体
    class BaseViewModel: NSObject {
        // 记录当前的索引值
        var index: Int = 1
    
        struct JunInput {
            // 网络请求类型
            let category: JunNetworkTool
    
            init(category: JunNetworkTool) {
                self.category = category
            }
        }
    
        struct JunOutput {
            // tableView的sections数据
            let sections: Driver<[AnchorSection]>
            // 外界通过该属性告诉viewModel加载数据(传入的值是为了标志是否重新加载)
            let requestCommond = PublishSubject<Bool>()
            // 告诉外界的tableView当前的刷新状态
            let refreshStatus = Variable<JunRefreshStatus>(.none)
    
            //初始化时,section的数据
            init(sections: Driver<[AnchorSection]>) {
                self.sections = sections
            }
        }
    }
    

    4-4. 自定义AnchorViewModel

    • 1) 继承BaseViewModel
    class AnchorViewModel : BaseViewModel{
        // 存放着解析完成的模型数组
        let anchorArr = Variable<[AnchorModel]>([])
    
    }
    • 2) 遵循JunViewModelType协议
    extension AnchorViewModel: JunViewModelType {
        typealias Input = JunInput
        typealias Output = JunOutput
    
        func transform(input: AnchorViewModel.JunInput) -> AnchorViewModel.JunOutput {
            let sectionArr = anchorArr.asDriver().map { (models) -> [AnchorSection] in
                // 当models的值被改变时会调用
                return [AnchorSection(items: models)]
            }.asDriver(onErrorJustReturn: [])
    
            let output = JunOutput(sections: sectionArr)
    
            output.requestCommond.subscribe(onNext: { (isReloadData) in
                self.index = isReloadData ? 1 : self.index + 1
                //开始请求数据
                junNetworkTool.request(JunNetworkTool.getHomeList(page: self.index))
                    .mapObjectArray(AnchorModel.self)
                    .subscribe({ (event) in
                        switch event {
                        case let .next(modelArr):
                            self.anchorArr.value = isReloadData ? modelArr : (self.anchorArr.value) + modelArr
                            SVProgressHUD.showSuccess(withStatus: "加载成功")
                        case let .error(error):
                            SVProgressHUD.showError(withStatus: error.localizedDescription)
                        case .completed:
                            output.refreshStatus.value = isReloadData ? .endHeaderRefresh : .endFooterRefresh
                        }
                }).addDisposableTo(bag)
            }).addDisposableTo(bag)
    
            return output
        }
    }
    • sectionArr是将model数组按照section分别存储
    • 当请求回来的anchorArr数据改变的时候, sectionArr随之会发生改变
    • isReloadData用于区分是下拉刷新(true时), 还是上拉加载更多(false时)

    5. RxCollectionViewController控制器中

    • 创建数据源RxDataSources
    • 绑定cell
    • 初始化input和output请求
    • 绑定section数据
    • 设置刷新

    5-1. 创建数据源RxDataSources

    // 创建一个数据源属性,类型为自定义的Section类型
    let dataSource = RxCollectionViewSectionedReloadDataSource<AnchorSection>()

    5-2. 绑定cell(自定义的cell要提前注册)

    dataSource.configureCell = { dataSource, collectionView, indexPath, item in
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: kCollecCellID, for: indexPath) as! RxCollectionViewCell
        cell.anchorModel = item
        return cell
    }
    • 以上四个参数的顺序分别为:dataSource, collectionView(或者tableView), indexPath, model, 其对应类型不言而喻,不多做介绍

    5-3. 初始化input和output请求

    let vmInput = AnchorViewModel.JunInput(category: .getNewList)
    let vmOutput = anchorVM.transform(input: vmInput)

    5-4. 绑定section数据

    //4-1. 通过dataSource和section的model数组绑定数据(demo的用法, 推荐)
    vmOutput.sections
        .asDriver()
        .drive(collectionVIew.rx.items(dataSource: dataSource))
        .addDisposableTo(bag)

    5-5. 设置刷新

    5-5-0. 在controller中初始化刷新状态

    collectionVIew.mj_header = MJRefreshNormalHeader(refreshingBlock: {
        vmOutput.requestCommond.onNext(true)
    })
    collectionVIew.mj_header.beginRefreshing()
    
    collectionVIew.mj_footer = MJRefreshAutoNormalFooter(refreshingBlock: {
        vmOutput.requestCommond.onNext(false)
    })

    5-5-1. 添加刷新的序列

    • 在JunOutput的结构体中添加刷新序列
    • 我们在进行网络请求并得到结果之后,修改refreshStatus的value为相应的JunRefreshStatus项
    • MJRefre遍会根据该状态做出相应的刷新事件
    • 默认状态为none
    // 告诉外界的tableView当前的刷新状态
    let refreshStatus = Variable<JunRefreshStatus>(.none)

    5-5-2. 外界订阅output的refreshStatus

    • 外界订阅output的refreshStatus,并且根据接收到的值进行相应的操作
    • refreshStatus每次改变都会触发刷新事件
    //5. 设置刷新状态
    vmOutput.refreshStatus.asObservable().subscribe(onNext: { (status) in
        switch status {
        case .beingHeaderRefresh:
            self.collectionVIew.mj_header.beginRefreshing()
        case .endHeaderRefresh:
            self.collectionVIew.mj_header.endRefreshing()
        case .beingFooterRefresh:
            self.collectionVIew.mj_footer.beginRefreshing()
        case .endFooterRefresh:
            self.collectionVIew.mj_footer.endRefreshing()
        case .noMoreData:                   
            self.collectionVIew.mj_footer.endRefreshingWithNoMoreData()
        default:
            break
        }
    }).addDisposableTo(bag)

    5-5-3. output提供一个requestCommond用于控制是否请求数据

    • PublishSubject 的特点:即可以作为Observable,也可以作为Observer,说白了就是可以发送信号,也可以订阅信号
    • 当你订阅PublishSubject的时候,你只能接收到订阅他之后发生的事件。subject.onNext()发出onNext事件,对应的还有onError()和onCompleted()事件
    // 外界通过该属性告诉viewModel加载数据(传入的值是为了标志是否重新加载)
    let requestCommond = PublishSubject<Bool>()

    七. 总结

    • 为了研究RxSwift相关知识, 工作之余的时间,差不多一个月了
    • 学习的瓶颈大部分在于网络请求和配合刷新这一模块
    • 文中如出现self循环引用的问题,还望大神多多指正
    • 小编目前也还在初学阶段,文中如出现小错误还望多多指正,如有更好的方法,也希望不吝分享
    • 如果喜欢,可以收藏,也可以在Github上star一下

    最后再一次附上Demo地址

    参考文献:

    展开全文
  • Moya的设计之道

    2017-08-11 13:44:52
    Moya是一个基于Alamofire开发的,轻量级的Swift网络层。Moya的可扩展性非常强,可以方便的RXSwift,PromiseKit和ObjectMapper结合。 如果你的项目刚刚搭建,并且是纯Swift的,非常推荐以Moya为核心去搭建你的网络层...

    前言

    Moya是一个基于Alamofire开发的,轻量级的Swift网络层。Moya的可扩展性非常强,可以方便的RXSwift,PromiseKit和ObjectMapper结合。

    如果你的项目刚刚搭建,并且是纯Swift的,非常推荐以Moya为核心去搭建你的网络层。另外,如果你对Alamofire的源码感兴趣,推荐我之前的一篇博客:

    Moya除了依赖Alamofire,还依赖Result。Result用一种枚举的方式提供函数处理结果:

    • .success(let data) // 成功,关联值是数据
    • .falure(let error) // 失败, 关联值是错误原因

    本文的讲解顺序:Moya的实现原理 -> Moya的设计理念 -> Moya与RxSwift,ObjectMapper一起工作

    接口

    分析任何代码都是从它的接口开始的。

    我们先来看看通过Moya如何去写一个网络API请求。Moya中,通过协议TargetType来表示这是一个API请求。

    协议要求提供以下属性,

    public protocol TargetType {
        var baseURL: URL { get }
        var path: String { get }
        var method: Moya.Method { get }
        var parameters: [String: Any]? { get } //参数
        var parameterEncoding: ParameterEncoding { get } //编码方式
        var sampleData: Data { get }//stub数据
        var task: Task { get }//请求类型,Data/Downlaod/Upload
        var validate: Bool { get } //是否需要对返回值验证,默认值false
    }

    通过枚举来管理一组API,比如

    public enum GitHub {
        case zen
        case userProfile(String)
    }
    extension GitHub: TargetType {
        public var baseURL: URL { return URL(string: "https://api.github.com")! }
        public var path: String {
            switch self {
            case .zen:
                return "/zen"
            case .userProfile(let name):
                return "/users/\(name.urlEscaped)"
            }
        }
        //....
    }

    当然也可以让你的Class/Stuct来实现TargetType协议,使用枚举可以方便的管理一组API,优点是方便复用baseURL,method等,缺点是不得不写大量的Switch语句

    然后,在进行API请求的时候,要创建MoyaProvider,接着调用Request方法进行实际的请求

    let provider = MoyaProvider<GitHub>()
    provider.request(.zen) { result in
        if case let .success(response) = result {
    
        }    
    }

    可以看到,Moya通过协议来定义一个网络请求,并且属性都是只读的。协议意味着是依赖于抽象,而不是具体的实现,这样更易控制藕合,并且容易扩展;只读的意味着不可变状态,不可变状态会让你的代码行为可预测。


    模块

    通过功能划分,Moya大致分为几个模块

    • Request,包括TargetType,Endpoint,Cancellable集中类型
    • Provider,网络请求的枢纽,Provider会把TargetType转换成Endpoint再转换成URLRequest交给Alamofire去实际执行
    • Response,回调给上层的数据结构,支持filtermapJSON等方法
    • Alamofire封装,通过桥接的方式对上层隐藏alamofire的细节
    • Plguins,插件。moya提供了插件来给给外部。包括四个方法,这里知道方法就好,后文会具体的讲解插件的方法在何时工作。

      public protocol PluginType {
          func prepare(_ request: URLRequest, target: TargetType) -> URLRequest
          func willSend(_ request: RequestType, target: TargetType)
          func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType)
          func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError>
      }

    原理

    为了更好的讲解Moya的处理流程,我画了一张图(用Sketch画的):

    第一眼看到这张图的时候,你肯定是困惑的,我们来一点点讲解图中的过程。通过上文的讲解我们知道,Provider这个类是网络请求的枢纽,它接受一个TargetType(请求),并且通过闭包的方式给上层回调。

    那么,我们来看看Provider的初始化方法:

    public init(endpointClosure: @escaping EndpointClosure = MoyaProvider.defaultEndpointMapping,
                requestClosure: @escaping RequestClosure = MoyaProvider.defaultRequestMapping,
                stubClosure: @escaping StubClosure = MoyaProvider.neverStub,
                manager: Manager = MoyaProvider<Target>.defaultAlamofireManager(),
                plugins: [PluginType] = [],
                trackInflights: Bool = false) {
      //...
    }

    初始化的时候的几个参数:

    • endpointClosure 作用是把TargetType转换成EndPoint,EndPoint是Moya网络请求的一个中间态。
    • requestClosure 作用是把Endpoint转换成URLRequest
    • stubClosure 是用来桩测试的,也就是模拟服务端假数据,这里先不管。
    • manager,实际请求的Alamofire的SessionManager
    • plugins, 插件
    • trackInflights,是否要跟踪重复网络请求

    Request

    在Moya中,请求是按照如图的方式进行转换的。其中,TargetType到Endpoint的转换是通过闭包endpointClosure来完成的。闭包的输入是TargetType,输出是EndPoint

    public typealias EndpointClosure = (Target) -> Endpoint<Target>

    在初始化Provider的时候,endpointClosure有默认参数,可以看到默认实现只是由Target创建了一个Endpoint

    public final class func defaultEndpointMapping(for target: Target) -> Endpoint<Target> {
        return Endpoint(
            url: url(for: target).absoluteString,
            sampleResponseClosure: { .networkResponse(200, target.sampleData) },
            method: target.method,
            parameters: target.parameters,
            parameterEncoding: target.parameterEncoding
        )
    }

    接着,通过requestClosure将Endpoing映射到URLRequest。这是你最后修改Request的机会,同样它也有默认参数。

    
    public final class func defaultRequestMapping(for endpoint: Endpoint<Target>, closure: RequestResultClosure) {
        if let urlRequest = endpoint.urlRequest {//urlReuqest有效,就以success执行闭包
            closure(.success(urlRequest))
        } else {//无效,以faliure执行闭包
            closure(.failure(MoyaError.requestMapping(endpoint.url)))
        }
    }

    为什么要用闭包进行TargetType->Endpoint->URLRequest映射呢?

    为了在灵活性和易用性之间进行平衡

    对于大部分API请求来说,使用Moya提供的默认闭包映射足以,这样大多数时候根本不需要关心着两个闭包的内容。但是有时候,有一些额外需求,比如对所有API请求增加额外的HTTP Header,moya通过闭包的方式开发者可以去修改这些内容。

    let endpointClosure = { (target: MyTarget) -> Endpoint<MyTarget> in
        let defaultEndpoint = MoyaProvider.defaultEndpointMapping(for: target)
        return defaultEndpoint.adding(newHTTPHeaderFields: ["APP_NAME": "MY_AWESOME_APP"])
    }

    为什么要引入requestClosure,把底层的URLRequest暴露给外部?

    我想有几点原因

    • 有些信息只有URLRequest创建之后才能知晓,比如cookie。
    • URLRequest属性很多,大多不常用,比如allowsCellularAccess,没必在Moya这一层封装。
    • Endpoint到URLRequest的映射是通过闭包回调的方式进行的,意味着你可以异步回调。

    为什么要引入Endpoint,不直接映射成URLRequest?也就是说,两步闭包映射变成一步

    为了保证TargetType维持不可变状态(属性全都是只读),同时给外部友好的API。通过Endpoint你可以方便的:添加新的参数,添加HttpHeader….


    Stub

    这里我们先不管流程图中的Plugins(插件),先顺着流程走,接下来我们到了一个叫做stub的模块。stub是一个测试相关的概念,通过stub你可以返回一些假数据。

    Moya的stub原理很简单,如果Provider决定Stub,那么就返回Endpoint中的假数据;否则就进行实际的网络请求。

    Moya通过StubClosure闭包开决定stub的模式:

    public typealias StubClosure = (Target) -> Moya.StubBehavior

    模式分为三种

    public enum StubBehavior {
        case never //不Stub
        case immediate //立刻返回数据
        case delayed(seconds: TimeInterval)//延时返回数据
    }

    返回数据的时候,就是简单的根据EndPoint中的假数据闭包:

    switch endpoint.sampleResponseClosure() {
        case .networkResponse(let statusCode, let data):
            let response = Moya.Response(statusCode: statusCode, data: data, request: request, response: nil)
            completion(.success(response))
            //...
    }

    默认的Endpoint的sampleResponseClosure

    sampleResponseClosure: { .networkResponse(200, target.sampleData) },

    Moya采用了这种简单粗暴,但是效果却很好的stub方式。

    这里很多人肯定会问,假如我不用Moya,我还想返回假数据,我该咋么做呢?

    答案是URLProtocol。通过URLProtocol可以拦截网络请求,你可以把网络请求重定向到假数据。

    对于NSURLConnection发起的请求可以直接拦截。在拦截NSURLSession的时候有一点tricky,因为URLSession支持的拦截是通过URLSessionConfiguration的属性protocolClasses来决定的,一般的做法是hook URLSession的初始化方法init(configuration: URLSessionConfiguration, delegate: URLSessionDelegate?, delegateQueue: OperationQueue?),然后把想要的拦截Protocol注册到URLSessionConfiguration中。


    Plugin

    Plugin提供了一种插件的机制让你可以在网络请求的关键节点插入代码,比如显示小菊花扽等。

    这里我们再看一下这张图,可以清楚的看到四个plugin方法作用的时机。

    Note:Plugin没有用范型编程,所以不要尝试在plugin中进行JSON解析然后传递给上层。

    Moya提供了四种Plugin:


    Response

    Moya并没有对Response进行特殊处理,仅仅是把Alamofire层面返回的数据封装成Moya.Response,然后再调用convertResponseToResult进一步封装成Result<Moya.Response, MoyaError>类型交给上层

    public func convertResponseToResult(_ response: HTTPURLResponse?, request: URLRequest?, data: Data?, error: Swift.Error?) ->
        Result<Moya.Response, MoyaError> {
            switch (response, data, error) {
            case let (.some(response), data, .none):
                let response = Moya.Response(statusCode: response.statusCode, data: data ?? Data(), request: request, response: response)
                return .success(response)
            case let (_, _, .some(error)):
                //....
            }
    }
    

    如果你要对Response进一步转换成JSON,可以用Response的方法,比如:

    func mapJSON(failsOnEmptyData: Bool = true) throws -> Any {/* */}

    到这里,Moya做的事情已经很清晰了:提供一种面向协议的接口来进行网络请求的编写;提供灵活的闭包接口来自定义请求;提供插件来让客户端在各个节点去介入网络请求;返回原始的请求数据给层。

    Moya最大的优点:

    • 纯粹的轻量级网络层。

    Cancel

    网络API请求应该是可以被取消的。也就是说,在发起一个API请求后,客户端应该能够有一个数据结构能够取消这个请求。Moya返回协议Cancellable给客户端

    public protocol Cancellable {
        var isCancelled: Bool { get }
        func cancel()
    }

    这符合《最少知识原则》。客户端不知道请求是什么,它唯一能做的就是cancel

    在内部实现中,引入了一个CancellableWrapper来进行实际的Cancel动作包装,返回的实际实现协议的类型就是它

    internal class CancellableWrapper: Cancellable {
        internal var innerCancellable: Cancellable = SimpleCancellable()
    
        var isCancelled: Bool { return innerCancellable.isCancelled }
    
        internal func cancel() {
            innerCancellable.cancel()
        }
    }
    
    internal class SimpleCancellable: Cancellable {
        var isCancelled = false
        func cancel() {
            isCancelled = true
        }
    }

    为什么要用一个CancellableWrapper进行包装呢?

    原因是:

    • 对于没有实际发出的请求(参数错误),cancel动作直接用SimpleCancellable即可。
    • 对于实际发出的请求请求,cancel则需要取消实际的网络请求。
    let cancellableToken = CancellableWrapper()
    if error{ //参数出错
        return cancellableToken
    }
    cancellableToken.innerCancellable = CancellableToken(request:request)

    CancellableToken中,取消网络请求:

    public final class CancellableToken: Cancellable{
        //...
        fileprivate var lock: DispatchSemaphore = DispatchSemaphore(value: 1)
        public func cancel() {
            _ = lock.wait(timeout: DispatchTime.distantFuture)
            defer { lock.signal() }
            guard !isCancelled else { return }
            isCancelled = true
            cancelAction()
        }
        init(request: Request) {
            self.request = request
            self.cancelAction = {
                request.cancel()
            }
        }
        //...
    }

    这里用到了信号量,为了防止两个线程同时执行cancel操作。


    Alamofire封装

    Moya采用桥接的方式,把Alamofire的API细节进行封装,详细的封装细节可见Moya+Alamofire.swift。总的来说,采用了两种方式:

    简单的类型桥接

    //用typealias进行桥接
    public typealias Method = Alamofire.HTTPMethod
    public typealias ParameterEncoding = Alamofire.ParameterEncoding

    协议桥接

    Alamofire对外的接口是Request类型。而Moya需要在Plugin中对Reuqest进行暴露,用协议怼Request进行了桥接

    public protocol RequestType {
        var request: URLRequest? { get }
        func authenticate(user: String, password: String, persistence: URLCredential.Persistence) -> Self
        func authenticate(usingCredential credential: URLCredential) -> Self
    }
    
    internal typealias Request = Alamofire.Request
    extension Request: RequestType { }

    然后,暴露给外部的接口变成了:

    func willSend(_ request: RequestType, target: TargetType)

    采用桥接的方式对外隐藏了细节,这样即使有一天Moya的底层依赖不再是Alamofire,对上层也没有任何影响。

    设计原则

    moya的很多设计原则是值得借鉴的,这些原则在软件开发领域是通用的。

    面向协议

    Swift是一个面向协议的语言。(这句话我好像在博客里写过好多遍了)

    比如:

    protocol TargetType {} //表示这是一个API请求
    public protocol Cancellable{}//唯一确定请求,只有一个接口用来取消
    public protocol RequestType{}//对外提供的请求类型,隐藏Alamofire的细节
    public protocol PluginType{} //插件类型

    面向协议的最大优点是:

    • 协议是建立的是一个抽象的依赖关系。

    同时,Swift协议支持扩展,你可以通过协议扩展为协议中的方法提供默认实现

    public extension TargetType {
        var validate: Bool {
            return false
        }
    }

    不可变状态

    不可变状态会让你的代码可预测,可测试。

    不可变状态是函数式编程里的一个核心概念。在Moya中,很多状态都是不可变的。典型的是:

    public protocol TargetType {
        var baseURL: URL { get } //只读
        var path: String { get } //只读
        //...
    }

    同样,还体现在Endpoint中:

    open class Endpoint<Target> {
        open let url: String //常量
        open let method: Moya.Method
        //...
        //不修改自身,而是返回一个新的实例
         open func adding(newHTTPHeaderFields: [String: String]) -> Endpoint<Target> {
            return adding(httpHeaderFields: newHTTPHeaderFields)
        }
    }

    高阶函数

    Swift中,函数是一等公民,意味着你可以把它作为函数的参数和返回值。当一个函数作为函数参数或者返回值的时候,称之为高阶函数。

    高阶函数让你的代码可以输入/输出逻辑,这样就增加了灵活性。

    比如在Provider初始化的时候传入的三个闭包:

    endpointClosure: = MoyaProvider.defaultEndpointMapping,
    requestClosure: = MoyaProvider.defaultRequestMapping,
    stubClosure: = MoyaProvider.neverStub,

    高阶函数配合函数默认值,是Swift开发中进行接口暴露的常用技巧。

    插件

    插件是我认为Moya这个框架最吸引我的地方。

    通过在各个节点暴露出插件的接口,让Moya的日志,授权,小菊花等功能无需耦合到核心代码里,同时也给外部足够的灵活性,能够插入任何想要的代码。

    类型安全

    使用枚举来保证类型安全是Swift中常用技巧。

    比如:

    //返回假数据
    public enum EndpointSampleResponse {
        case networkResponse(Int, Data)
        case response(HTTPURLResponse, Data)
        case networkError(NSError)
    }
    

    错误处理

    Moya的错误处理主要采用了两种方式:

    抛异常:

    public func filterSuccessfulStatusAndRedirectCodes() throws -> Response {
        return try filter(statusCodes: 200...399)
    }

    Result类型:

    func convertResponseToResult(****) -> Result<Moya.Response, MoyaError> {
        return .success(response) 
        return .failure(error)
    }

    在Swift中,通过Result类型来处理异步错误是一个很常见也很有效的做法。

    使用Result类型最大的好处是可以不用每一步都处理错误。

    比如,类似这个链式调用,每一步都有可能出错,通过Result类型,我们可以在最后统一处理错误。

    provider.request(...).filter().mapJSON.filter().{ result in
        switch result {
            case let .success(moyaResponse):
    
            case let .failure(error):
        }
     }
    

    RxSwift

    RxSwift是一个响应式编程框架,它是语言层面的扩展,改变的是你写代码的方式,与具体业务细节无关。

    如果你对RxSwift并不熟悉,推荐我之前的一篇博客:RxSwift使用教程。另外,我还维护了一个awesome-rxswift列表。

    Moya核心代码并没有支持RxSwift,那样就与另外一个框架耦合在一起了。Moya采用了扩展的方式,让Moya支持RxSwift,具体代码参见RxMoya

    在扩展中,提供了RxMoyaProvider类:

    class RxMoyaProvider<Target>: MoyaProvider<Target>

    在请求的时候,不再通过闭包进行回调,而是返回Observable<Response>(一个可监听的信号源)。

    open func request(_ token: Target) -> Observable<Response> {
        return Observable.create { observer in 
            //...
        }
    }

    然后,通过extension扩展ObservableType为Response提供各种响应式处理方法

    extension ObservableType where E == Response {
         public func mapJSON(failsOnEmptyData: Bool = true) -> Observable<Any>
         public func filter(statusCode: Int) -> Observable<E>
    }

    ObjectMapper

    ObjectMapper 是一个用来做把JSON转换成Struct/Class的Swift框架。

    实际开发中,先把JSON转换成对象再进行下一步UI操作是很常见的事情。结合RxSwift,我们可以很容易的把ObjectMapper插入响应式处理的一个节点中:

    extension ObservableType where E == Response {
        public func mapObject<T: Mappable>(_ type: T.Type) -> Observable<T> {
            return flatMap { response -> Observable<T> in
                return Observable.just{/* 这里引入ObjectMapper进行JSON解析*/}
            }
        }
    }

    通过这个方法,可以进行信号中包含的信息转换:

    于是,通过RxSwift和ObjectMapper,就可以这么处理:

    rxRrovider.request(.targetType)
      .mapObject(YouClass.type)
      .subscribe { event -> Void in
        switch event {
        case .next(let object):
          self.object = object
        case .error(let error):
          print(error)
        default:
          break
        }
      }.addDisposableTo(disposeBag)

    展开全文
  • Swift 5 Type Metadata 详解

    2019-03-01 15:36:19
    Swift 5 出了,主要是 ABI 稳定了,从 ABI Dashboard 来看为了解决 ABI 稳定问题,对 type metadata 也有不少改动。众所周知,我们 App 的 JSON 库 HandyJSON 是强依赖 metadata 结构的,如果 metadata 有大规模的...
  • Moya基本用法

    2019-02-27 23:34:13
    Moya简介 一句话来讲,Moya是对Alamofire的封装,让开发人员更加优雅的使用Alamofire。 基本用法 定义接口枚举 public enum HFAPI { case login(String, String, String) //登录接口 case smscode(String) //...
  • Alamofire 可以通过CocoaPods 导入,也可以下载导入,这里是初学,CocoaPods 还不太熟悉,就直接下载导入使用了 1、Alamofire 下载 2、Alamofire 导入 3、Alamofire 使用 1、Alamofire 下载 ...
  • GitHub上Swift开源项目!

    2017-02-04 19:10:16
    swift-open-project这里汇集了目前为止最为流行的Swift开源项目,选取SwiftGuide中的开源部分,这里将每周对项目进行总结,如果大家有推荐的可以联系thinkloki@gmail.com,或者issues,欢迎Star、Fork。感谢...
  • RxSwift+Moya之项目实战 RxSwift相关基本介绍和用法可参考: RxSwift的使用详解01 RxSwift的使用详解02 一. 下面将将进行实战项目 1.登录注册功能 输入用户名要大于6个字符,不然密码不能输入 密码必须大于6个...
  • 2019独角兽企业重金招聘Python工程师标准>>> ...
  • 最近刚刚把接手的OC项目搞定,经过深思熟虑后,本人决定下个项目起就使用Swift(学了这么久的Swift还没真正用到实际项目里。。。),而恰巧RxSwift已经出来有一些时间了,语法也基本上稳定,遂只身前来试探试探这...
  • Swift 语言指南

    2015-07-03 10:09:06
    这份指南汇集了 Swift 语言主流学习资源,并以开发者的视角整理编排。 GitHub:ipader/SwiftGuide | 网站:http://dev.swiftguide.cn | 欢迎开发者一起维护,或反馈/投稿 想了解关于该指南及 Swift 更多信息...
  • Swift语言指南

    2015-01-14 13:35:25
    这份指南汇集了Swift语言主流学习资源,并以开发者的视角整理编排。 GitHub: ipader/SwiftGuide | 网站:http://dev.swiftguide.cn | 欢迎开发者一起维护,或反馈/投稿 @SwiftLanguage 更新于 2015-1-11,更新...
  • RxSwift

    2019-08-06 00:11:30
    详解 文档 Swift - RxSwift的使用详解1(基本介绍、安装配置) Swift - RxSwift的使用详解2(响应式编程与传统式编程的比较样例) Swift - RxSwift的使用详解3(Observable介绍、创建可观察序列) Swift - RxSwift...
  • swift笔记

    2020-02-04 10:11:11
    Swift5.1 https://docs.swift.org/swift-book/LanguageGuide/Subscripts.html 1、swift没有隐式转化 Swift中没有隐式转化,不会将整形自动转成浮点型 let m = 32 let n = 3.14 let result = m + n 错误写法 ...
  • 【iOS】仿知乎日报,RxSwift-Part1-首页搭建 2017年10月21日 09:42:42 阅读数:738 前言 之前的几篇博客算是入门篇,那么这篇就是RxSwift的实战篇。由于对RxSwift的认识还不够深刻,所以项目中没有使用MVVM模型...
  • TimLiu-iOS Swift版本点击这里 Objective-C版本点击这里 欢迎加入QQ群交流: 594119878 更新日期:2018-4-11 About A curated list of iOS objective-C ecosystem. How to Use ... to search ...
1 2
收藏数 28
精华内容 11
热门标签
关键字:

moya详解 swift