OC转Swift之路之着手搭建基于MVVM模式并且依赖RxSwift的Swift项目架构

概要

要开始做一个项目,首先要搭建好这个项目的架构,一个项目架构的好坏直接影响到一个项目以后的维护性扩展性和装逼性等一系列的问题。
在最近一个新的项目,我用到Swift来写,因为对Swift不是很熟悉,所以前期做了许多工作,看别人的项目源码啊,看视频什么。目的是尽量了解一个Swift的项目的架构应该如何去做。
在思考这个项目的架构的过程中,考虑修改的比较多的就是网络层(接口调用是否方便,接口添加是否容易,是否符合Swift的风格.......)。

另外,这次项目也用到了RxSwift,基于MVVM模式,但并不严格遵循(主要是懒和觉得有些地方没必要)。项目中也比较多的地方用到OC没有而且实用的Swift的泛型特性。
项目的swift版本是2.3。

1.项目的目录架构

22.png
  • 1 基础父类
  • 2 类库(接下来会介绍)
  • 3 弹窗类
  • 4 扩展
  • 5 网络层(MNNetwork网络请求单例,接口地址,路由-用于请求体的封装-接下来会介绍)
  • 6 其他资源
  • 7 公共类(主要介绍MNModel,数据模型层)
  • 8 三方库

2.接口地址文件-Interface.swift

struct Interface {
    enum MNService {
        case release, test // 定义服务器地址,方便生产服和测试服们之间的切换
    }
    static let kService = MNService.test // 默认
    static var kHost: String {
        get {
            if (Interface.kService == HLService.release) {
                return "http://release.cn/" 
            }
            else if (Interface.kService == HLService.test) {
                return "http://test.cn/"
            }
            return ""
        }
    }
    static let Test1 = Interface.kHost + "1" // 一个测试接口
}

3.网络请求路由-MNRouter.swift

一个文件里面我会定义不止一个请求路由,例如我会定义一个涉及用户操作的路由和不涉及用户操作的路由,因为他们的请求体可能会不同,前者需要加解密操作,而后者则不需要,这一定程度上是为了应对2017年新的审核规则。
let AF = Alamofire.ParameterEncoding.URL // 相当于宏,方便接下来的调用
// 登录注册Router
enum LoginAndRegisterRouter: URLRequestConvertible {
    // 登录,这里之定义了一个接口
    case Login(phone: String, pwd:String)
    // relation会根据该枚举实例的类型返回相对应的元组,包括请求方法(GET,POST....),请求路径,请求参数
    var relation: (method: Alamofire.Method, path: String, params: [String: String]?) {
        switch self {
        case Login(let phone, let pwd):
            return (Alamofire.Method.POST, Interface.Login, ["name": phone, "pwd": pwd])
        }
    }
    // 返回封装好的请求体,接下来的调用接口操作就很方便了
    var URLRequest: NSMutableURLRequest {
        let url = NSURL.init(string: relation.path)
        let mRequest = NSMutableURLRequest.init(URL: url!)
        mRequest.HTTPMethod = relation.method.rawValue
        mRequest.setValue("xxx", forHTTPHeaderField: "token")
        
        return AF.encode(mRequest, parameters: relation.params).0
    }
}

该枚举遵循了Alamofire 的URLRequestConvertible协议,里面要求实现一个URLRequest计算属性。
看了一些项目的源码和网上关于Alamofire的最佳实践的视频,它们的做法是relation那里method独立开来,然后switch一次;然后是path,URLRequest都一样的做法。后来我想了想,搞个元组类型的relation来关联这些更为方便些,因为我发现项目中的请求体基本是一样的,所以没必要做那么多功夫。想想,一个接口添加要多2个步骤,那如果一下子做几十个接口的添加的话,工作量会比较大。

4.数据模型的构建-MNModel.swift

protocol MNModelProtocol {
    static func modelWithJSONMN(dict: [String: AnyObject]) -> Self?
}
class MNModel: NSObject, MNModelProtocol {
    static func modelWithJSONMN(dict: [String : AnyObject]) -> Self? {
        return self.modelWithJSON(dict)
    }
    func modelArray<T: NSObject>(dic: NSDictionary, key: String, type: T) -> Array<T> {
        var temp: [T] = []
        if dic[key] is NSArray {
            for item in dic[key] as! NSArray {
                let tempItem = item as? NSDictionary
                // modelWithJSON是YYModel定义的一个类方法,?? 值为空就传一个默认值
                temp.append(T.modelWithJSON(tempItem ?? []) ?? T())
            }
        }
        return temp
    }
    // 打印数据
    override var debugDescription: String {
        return self.modelDescription()
    }
    override var description: String {
        return self.modelDescription()
    }
}
// 数据模型例子
class ModelClass: MNModel {
    var identity: IdentityClass = IdentityClass()
    var students: Array<Student> = []
    class func modelContainerPropertyGenericClass() -> NSDictionary {
        return [
            "identity": IdentityClass.self,
        ]
    }
    func modelCustomTransformFromDictionary(dic: NSDictionary) -> Bool {
        students = self.modelArray(dic, key: "students", type: Student())
        return true
    }
}

项目里面YYModel替代了MJExtension,YYWebImage替代了SDWebimage,YYKit还有其他好用的库,例如YYText,一次pod,多处可用。
其实YYModel兼容Swift并不是很好,像我们经常碰到的数组里面存自定义类型的元素,通过YYModel,MJ转换的数组的遍历在Swift中都会崩溃,因为swift不支持由oc数组转换过来的这样的数组。所以自己做了一些处理。(我之前看见一篇文章,介绍过一个库是实现了Swift版的json转model的,忘记了)。
MNModel要遵循MNModelProtocol这样一个协议是为了让我能够实现请求回调的一个处理数据的泛型函数,下面会介绍。
第二个函数modelArray就是为了解决上面说到的一个问题。
其实可以感受得到在swift中使用泛型的地方会更多,oc是动态类型语言,它弱类型的特质使得我们可以很方便的实现类型之间的转换,而到了swift,他是静态语言,强类型,很多时候类型的转换操作更加的繁琐,甚至OC的转换思路去到Swift就报错,但是它带来的是程序更加的可靠和稳定。

5.网络请求单例的封装-MNNetwork.swift

// 枚举的泛型例子
enum NetWorkResult<T> {
    case value(T)
    case error(String)
}
typealias Finished = ((objc: [String: AnyObject]?, error: NSError?, badNetwork: BooleanType?) -> ())?
typealias Success = ((objc: AnyObject!) -> ())?
typealias ErrorFunc = ((objc: String!) -> ())?
class MNNetwork: Alamofire.Manager {
    // 这是封装了网络请求的单例
    static let tool: MNNetwork = {
        let config = NSURLSessionConfiguration.defaultSessionConfiguration()
        config.HTTPAdditionalHeaders = Manager.defaultHTTPHeaders
        config.timeoutIntervalForRequest = 60.0  // 设置超时
        config.HTTPMaximumConnectionsPerHost = 1 // 设置最大并发量
        return MNNetwork(configuration: config)
    }()
    
    class func shareNetwork() -> MNNetwork {
        return tool
    }
    // 这个函数的作用是根据传入的请求体进行网络请求,返回数据后进行一系列的判断数据的有效性(例如status=0就返回错误等)
    // 若果数据有效,进行json转Model的操作并返回NetWorkResult.value(tempModel),tempModel是对应数据模型的实例,之所以这样返回因为要适应RxSwift
    // 若果数据无效,则返回NetWorkResult. error(objc),枚举值是字符串,用以错误提示
    func responseJSONMN<T: MNModelProtocol>(request: URLRequestConvertible,
                        type: T,
                        success: ((T!) -> ())?,
                        fail: ((String!) -> ())?
                        ) -> Observable<NetWorkResult<T>> {
        
        return Observable.create { observer -> Disposable in
            
            let request = MNNetwork.shareNetwork().request(request).responseJSON { (response: Response<AnyObject, NSError>) in
                if response.result.isSuccess {
                    if let dict = response.result.value as? [String: AnyObject] {
                        let model = T.modelWithJSONMN(dict) // 获取数据模型实例
                        if let tempModel = model {
                            if let tempSuccess = success {
                                tempSuccess(tempModel) // 以上一大串是用于保证数据的安全性,这里相当于OC的block,返回数据模型实例
                            }
                            observer.onNext(NetWorkResult.value(tempModel))
                        }
                        else {
                            let objc = "数据解析失败"
                            if let tempFail = fail {
                                tempFail(objc)
                            }
                            observer.onNext(NetWorkResult.error(objc))
                        }
                    }
                    
                }
                else {
                    let objc = "网络异常"
                    
                    if let tempFail = fail {
                        tempFail(objc)
                    }
                    observer.onNext(NetWorkResult.error(objc))
                }
            }
            return AnonymousDisposable {
                request.cancel()
            }
        }
    }
}

这里说一下我对 .self Self .Type的理解吧:

  • .self : 例如UIViewController.self,相当于获取UIViewController的类实例,然后调用它的类方法。
  • Self : 一般用在类方法的返回值类型的声明,代表返回一个当前类型的实例。相当于instancetype
static func modelWithJSONMN(dict: [String : AnyObject]) -> Self? { 
      return self.modelWithJSON(dict)
 }
  • .Type : 用来声明该 类实例 是什么类型,它的作用仅用于声明,例如一个方法NSClassFromString(strClass) as! UITableViewCell.Type,NSClassFromString都知道是返回一个类实例,然后我需要在后面声明这个类实例属于什么类型。

6.网络请求的调用

func getLogin() -> Observable<NetWorkResult<MNLoginModel>> {
        let create = testRouter.Login(phone: "135111111111", pwd: "123456")
        return MNNetwork.shareNetwork().responseJSONMN(create, type: MNLoginModel(), success: { (model) in
       
            print("po:\(model)")
            
            }, fail: { (error) in
                  print("errorString:\(error)")
        })
    }

这里举了个登录接口调用的例子,至于返回值也是也是为了适应RxSwift。关于基于Rxswift的网络请求相关更多的资料可以看其他的文章Rxswift网络请求使用

总结

好了,一个基于MVVM模式依赖RxSwift开发的Swift项目架构就差不多是这样了,然后放出呆萌:Demo,呆萌里面手势滑动返回,tabbar显示隐藏动画等的封装我觉得应该对你的项目有帮助。项目中的不足之处希望能够得到大家的指点,谢谢~~!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,066评论 4 62
  • 转载自:https://github.com/Tim9Liu9/TimLiu-iOS 目录 UI下拉刷新模糊效果A...
    袁俊亮技术博客阅读 11,915评论 9 105
  • 海望 文:晓霞初阳 青山碧水雨随风, 海乌翔空比翼行。 夕日西垂晖浩瀚, 月下依楼百缕情。
    晓霞初阳阅读 163评论 1 1
  • 第二章 有匪君子 正值夏季,烈日炎炎,炙烤着大地。人间,就如硕大的蒸笼,充斥着闷热、焦躁、不安...
    魅雨之辰阅读 306评论 2 2