给Controller减肥

首页 / iOS / 正文

MVC

截屏2021-11-21 下午3.47.17.png

  • Model:数据模型,一个存储数据的对象
  • View:负责展示用户的界面
  • Controller:定义Model如何展示给View,并且View接受到事件后反馈到Model,承担Model和View的同步工作

Controller直接引用View和Model,View并不知道Controller,通过delegate或者闭包来进行传递。Model也不知道Controller,当Model发生改变时,通过Notification的方式来传递给Controller去更新View

在这个过程中,我们可以发现Controller承担了大量的工作

  • 网络请求
  • 数据读写
  • 数据处理
  • UI布局
  • 界面跳转
  • 处理View的回调
  • 监听Model

由于上述工作都在Controller里,Controller就成了一个超级大的巨无霸,为了给Controller瘦身,我们就引入MVVM

MVVM

截屏2021-11-21 下午4.09.35.png

MVVM则变成了Model - ViewModel - View,View持有对ViewModel的引用,反之没有,ViewModel持有对Model的引用,反之没有

通过ViewModel,我们把Controller中的数据和逻辑处理部分抽离出来,从而实现瘦身的目的

Controller瘦身

工厂模式

在一些UI控件的代码中,往往有很多重复的代码,这里我们拿UIlabel来举例

                        let label = UILabel()
            label.font = UIFont.systemFont(ofSize: 16)
            label.textColor = .black
            label.numberOfLines = 2
            label.lineBreakMode = .byTruncatingTail

在App中我们通常使用的是几种固定的字体,那么我们就可以通过工厂模式来生产不同的字体来实现代码复用

//定义类型
enum LabelStyle {
    case title
    case subTitle
}
//生产不同字体
static func with(style initalStyle: LabelStyle) -> UILabel {
        switch initalStyle {
        case .title:
            let label = UILabel()
            label.font = UIFont.systemFont(ofSize: 16)
            label.textColor = .black
            label.numberOfLines = 2
            label.lineBreakMode = .byTruncatingTail
            return label
        case .subTitle:
            let label = UILabel()
            label.font = UIFont.systemFont(ofSize: 12)
            label.textColor = .gray
            return label
        }
    }

甚至我们还可以加一个方法,让它直接链式的调用

@discardableResult
    func added(into superView:UIView) -> UILabel{
        superView.addSubview(self)
        return self
    }
    UILabel.with(style: .title).added(into: contentView)

初始化一个控价就有了更加好的方法,当然我们也要结合实际来设计这些方法

ViewModel

Controller解耦中,引入ViewModel,把Controller中对应与View相关的逻辑抽离出来,这样Controller需要做的就是

  • 从DB/网络中获取数据,转换成ViewModel
  • 把ViewModel装载给View
  • View的属性与ViewModel值绑定在一起

这里我是将ViewModel和Cell绑定在了一起,当ViewModel改变时,Cell的也会发生改变

class ViewModel {
    var model: Obserable<NewsData>
    required init(model: NewsData) {
        self.model = Obserable(value: model)
    }
}

class Obserable<T> {
    typealias ObserableType = (T) -> Void
    var value: T {
        didSet {
            observer?(value)
        }
    }

    var observer: (ObserableType)?
    func bind(to observer: @escaping ObserableType) {
        self.observer = observer
        observer(value)
    }

    init(value: T) {
        self.value = value
    }
}

拓展Cell,将起绑定

extension NewsTableViewCell {
    var obModel: Obserable<NewsData>.ObserableType {
        return { value in
            self.configUI(with: value)
        }
    }
}

//这段代码就是绑定

let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! NewsTableViewCell
viewModel[indexPath.row].model.bind(to: cell.obModel)

网络层封装

这里的封装就是对Alamofier进行上层封装,实际上我们还可以再对其进行一层封装,只暴露出具体的请求类型接口,让parameters等相关信息对于Controller是透明的,类似于透明传输这个概念

typealias NetworkSuccessBlock = (_ response: Any) -> Void

typealias NetworkFailureBlock = (_ error: Any) -> Void

struct NetModel: HandyJSON {
    var reason = "" //失败或者成功
    var result: AnyObject?
    var error_code = 0 // 错误码
}

class NetworkTool: NSObject {
    static let shared = NetworkTool()
    static let api = ".."
    override private init() {
    }
}

extension NetworkTool {
    public func requestWith(params: [String: Any]? = nil, success: @escaping NetworkSuccessBlock, error: @escaping NetworkFailureBlock) {
        getRequestWith(url: NetworkTool.api, params: params) { json in
            guard let model = NetModel.deserialize(from: json as? Dictionary) else { return }
            self.handelData(model: model, success: success, error: error)
        } error: { err in
            error(err)
        }
    }
}

extension NetworkTool {
    
    //最接近Alamofire
    private func getRequestWith(url: String, params: [String: Any]? = nil, success: @escaping NetworkSuccessBlock, error: @escaping NetworkFailureBlock) {
        Alamofire.request(url, method: .get, parameters: params, encoding: URLEncoding.default).responseJSON { response in
            switch response.result {
            case let .success(value):
                success(value)
            case let .failure(err):
                error(err)
            }
        }
    }
    
    private func handelData(model: NetModel, success: @escaping NetworkSuccessBlock, error: @escaping NetworkFailureBlock) {
        switch model.error_code {
        case 0:
            success(model.result as Any)
        default:
            error(model.reason)
        }
    }
}

路由

界面直接的跳转是一个无法回避的问题,你会发现在Controller中,绝对有如下代码

if indexPath.secion == 0{
    if indexPath.row == 0{
    
    }else if....
}else if indexPath.section == 1{

}

然后不断的引用不同的Controller,直到这段代码长的不能再长。

在Controller的结偶中,最常见的就是加一个中间层路由进行不同的跳转,我也小小的实现了一个

具体的流程如下:

  • ControllerA发起跳转请求Request
  • Router解析Request,轮询问各个Module,看看各个Module是否支持对应的Requst。
  • Router根据Module的Response,合成跳转
  • 导航根据command进行跳转,并且返回feedBack给Router
  • Router返回feedback给ControllerA

代码如下

protocol RouterProtocol {
    static func targetWith(pa: [String: Any]) -> RouterProtocol?
    func isPush() -> Bool
}

extension RouterProtocol {
    func isPush() -> Bool {
        return true
    }
}

class Router {
    private let httpString = "http,httpss"
    var targetDic = [String: RouterProtocol.Type]()
    let appScheme = "router"
    static let shared = Router()
    private init() {}

    func registerRouter(target: RouterProtocol.Type, key: String) {
        targetDic.updateValue(target, forKey: key)
    }

    func targetWith(url: String, externParameter: [String: Any]? = nil) -> RouterProtocol? {
        let encodeUrl = url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
        if let urlComponents = URLComponents(string: encodeUrl) {
            let scheme = urlComponents.scheme ?? ""
            let host = urlComponents.host ?? ""
            let path = urlComponents.path
            var parameter = [String: Any]()
            if let queryItems = urlComponents.queryItems {
                for query in queryItems {
                    parameter.updateValue(query.value ?? "", forKey: query.name)
                }
            }
            if let externDic = externParameter {
                for (key, value) in externDic {
                    parameter.updateValue(value, forKey: key)
                }
            }
            if scheme == appScheme {
                return targetWith(key: host + path, parameter: parameter)
            } else if httpString.contains(scheme) {
                if let target = targetWith(key: host + path, parameter: parameter) {
                    return target
                }
            }
        }

        return nil
    }

    func targetWith(key: String, parameter: [String: Any]) -> RouterProtocol? {
        if let router = targetDic[key] {
            return router.targetWith(pa: parameter)
        }
        return nil
    }
}

class RouterManager {
    
    static func registerRouters() {
        let router = Router.shared
        router.registerRouter(target: ViewController.self, key: "home/")
        router.registerRouter(target: WebViewController.self, key: "web/")
    }
    
    static func openUrl(url: String, externParameter: [String: Any]? = nil) {
        if let target = Router.shared.targetWith(url: url, externParameter: externParameter) {
            let isPush = target.isPush()
            if let vc = target as? UIViewController {
                self.openVC(vc: vc, isPush: isPush)
            } else {
                print("没有此模块")
            }
        }
    }
    
    static func openVC(vc: UIViewController, isPush: Bool) {
        if let topVC = UIViewController.topViewController() {
            if isPush {
                if topVC.navigationController != nil {
                    topVC.routerPushToVC(toVC: vc)
                } else {
                    let navi = UINavigationController(rootViewController: vc)
                    topVC.routerPresent(navi, animated: true, completion: nil)
                }
            } else {
                topVC.routerPresent(vc, animated: true, completion: nil)
            }
        }
    }
    
}

总结

上面这些方法,我参考了不少大牛的博客。这些方法都是为了代码能够便于理解和拓展,当然肯定还有我没见到过的方法,这正是我需要学习的。

这里附上项目地址,大家感兴趣的可以直接下载Demo

无标签
评论区
头像