iOS代码结构设计

文中代码前缀 DH 为公司代码前缀

近两天老大给我发了两篇关于安卓架构设计的文章,供我参考学习。虽然还没有接触过安卓的开发,但基本的思想捋了一下还是可以理解的。顺便总结一下,在iOS开发中使用。
资历尚欠不敢妄言架构,本文仅对它人文章梳理后于己所用。

附安卓架构文章:
Android项目重构之路:架构篇
Android项目重构之路:界面篇
Android项目重构之路:实现篇

1、架构篇

这篇文章写得是代码的层级划分:
架构层级图

  • 模型层:面向对象开发,对事物抽象的模型类。
  • 界面层:这个很容易理解,UI。
  • 核心层:业务逻辑处理,常用的MVC会把网络数据block回调到控制器处理业务与界面交互展示,但是该文章中将业务逻辑抽出来单独处理。
  • 接口层:即iOS中常说的网络层

2、界面篇

界面即UI,这里没什么好说的,主要强调的就是命名要规范,封装要合理,预留的接口简单易用。UI通过预留的调用接口仅接收展示的数据,不做业务逻辑判断和处理。

3、实现篇

实现中分别说说四个层级的代码。实现篇代码以登录为例代码实现。

3.1 模型层

根据网络返回的JSON数据格式:

{"status": "0", "message": "success"}
{"status": "0", "message": "success", "detailBean":{...}}
{"status": "0", "message": "success", "listDetailBean":[{...}, {...}], "currentPage": 1, "pageSize": 20, "maxCount": 2, "maxPage": 1}

最外层Model使用泛型,import HandyJSON为阿里开源的字典模型互转工具。OC的MJExtensin因 swift4.0 的语法中取消了@objcOC类型的自动推断,所以使用时需在Model类和各属性前手动添加@objc,略显麻烦,故改用HandyJSON框架。泛型 T 根据返回的数据类型而定。该泛型语法找了好久。。。

import UIKit
import HandyJSON

class DHAPIModel<T: DHBaseModel>: DHBaseModel {

    var status: Int = Int()
    var message: String = String()

    var currentPage: Int = Int()
    var pageSize: Int = Int()
    var totalCount:Int = Int()
    var totalPages: Int = Int()

    var detailBean: T?

    var listDetailBean: [T]?

    required init() {}
}

属性detailBean泛型T对应的类 DHLoginResultModel

import UIKit
import HandyJSON

class DHLoginResultModel: DHBaseModel {

    /// 用户姓名
    var realName: String?
    /// 用户ID
    var userId: Int!

    /// 公司ID
    var companyId: String! = ""
    /// 部门
    var deptName: String!
    /// 部门ID
    var deptId: String!
    /// 车队长电话
    var cheDuiZhangNum: String! = ""

    // 使用 HandyJSON 必须要实现该方法
    required init() {}
}

3.2 界面层

界面效果
登录界面效果
文末附上登录界面封装的代码。

登录的界面层做了封装,控制器直接调用即可:

private func setLoginView() {
    let loginView = DHLoginView(frame: self.view.frame)
    loginView.setBackgroundImage(imgName: "login_bg")
    lgView.delegate = self
    loginView.setLoginInfo(userName: DHLoginManager.shared.userName,
                           pwd: DHLoginManager.shared.password)
    self.view.addSubview(loginView)
}

// MARK: - DHLoginViewDelegate
func loginViewAction(userName: String, password: String) {
    // 此代理方法中完成登录操作
}

3.3 接口层

先来说接口层,之后聊核心层。安卓的架构中所说的接口即网络请求,接口interface即iOS中常说的协议,Listener即iOS中的Block回调。安装中定义的接口然后用实现类实现的方法,可以理解为面向协议的开发。面向协议的事情以后有时间再谈,现在要做的应该是弄清楚代码的结构,所以下面的做法就是登录的网络层接口。

iOS中我们还用比较熟悉的AFNetworkingAlamofire封装为单例类方法使用。
重点还是要说说Alamofire.SessionManager单例的默认配置属性URLSessionConfiguration.default

方法request(...)可以使用Alamofire直接调用的:Alamofire.request(...)。但对应超时时间等配置,我们就只能使用框架默认值。尤其超时时间默认时间太长。

而如果我们要自己定义超时时间,就需要用向下面的代码这样使用单例Alamofire.SessionManager

// 必须写成全局的,必须要被强引用
private var _sessionManager: Alamofire.SessionManager!
static var shared: Alamofire.SessionManager {
    let configuration = URLSessionConfiguration.default
    configuration.timeoutIntervalForRequest = 5
    _sessionManager = Alamofire.SessionManager(configuration: configuration)
    return _sessionManager
}

上面的代码并没有什么问题,但是测试后就会发现,对于同时发起的多个请求,只能成功一个。之前一直未找到原因,为了设置超时时间,便修改了源码中的参数,而在摸索测试中发现,修复该问题只需要添加一句代码即可:

// 不配置该属性,不能同时进行多个网络请求
configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders

很坑,然而百度上什么资料都找不到。。。文末附上自己思考后写下的Alamofire二次封装代码。

把上方的代码封装为DHBaseNetwork基类。至于登录的接口,在继承DHBaseNetwork的基础上,根据自身业务设计,此处我们写的接口就是登陆相关的网络接口类DHLoginNet

import UIKit

class DHLoginNet: DHBaseNetwork {

    typealias LoginResultHandle = (_ responseObject: DHLoginResultModel)->()

    /// 登录Net方法
    ///
    /// - Parameters:
    ///   - userName: 用户名
    ///   - password: 密码
    ///   - result: 返回的网络数据回调
    ///   - fail: 请求失败回调
    class func login(userName: String, password: String, result: @escaping LoginResultHandle, fail: @escaping FailHandle) {
        //  Use of undeclared type 'DataRequest'
        let params:  [String : Any] = ["userName": userName,
                                       "passWord": password,
                                       "clientMode": 1,
                                       "version": "1.0"]
        let path = kBaseAddress + kLoginAddress
        DHBaseNetwork.POST_JSON(path: path, params: params, success: { (response) in
        // 使用HandyJSON,返回值解析成Model
            let apiModel = DHAPIModel<DHLoginResultModel>.deserialize(from: response)
            result((apiModel?.detailBean)!)
        }) { (error) in
            fail(error)
        }
    }

    /// 取消登录请求
    class func cancelLogin() {
        DHBaseNetwork.cancelQuestion(withURL: kBaseAddress + kLoginAddress)
    }
}

以上接口层的目的就是为了从网络抓取数据。


3.4 核心层

Action,文中也提到了“它的核心任务就是处理复杂的业务逻辑”。本文的登录Demo中,将其命名为了DHLoginManager,将登录的业务逻辑抽出来写到该类中封装为单例使用。然后对登录的结果使用了通知发送出来。

此处需要说明的是,参数合法判断,和不需要控制器参与即可填写的参数一律在Action内部完成。使控制器与核心层尽可能的低耦合。因业务设计,此处并没有对登录的数据定向的回调给控制器,但是若有该需求可使用闭包函数(或Block)将处理后的业务数据回调给控制器,有MVP设计模式的味道了。

DHLoginManager中调用DHLoginNet的登录方法,实现登录、保存账号和密码以及用户信息等业务逻辑。

// MARK: - 公开调用方法
/// 登录
public func login() {
    if userName.lengthOfBytes(using: .utf8) == 0 || password.lengthOfBytes(using: .utf8) == 0 {
        NotificationCenter.default.post(name: NSNotification.Name(rawValue: kLogoutNotification), object: nil)
        return
    }
    DHLoginNet.login(userName: userName, password: password.md5, result: { [weak self] (loginResultModel) in
        // 登录成功,保存账号、密码信息
        self?.save(account: (self?.userName)!, pwd: (self?.password)!)
        // 保存用户信息
        ......
        // 发送登录成功的通知
        NotificationCenter.default.post(name: NSNotification.Name(rawValue: kLoginSuccessNotification), object: nil)
    }) { (error) in
        NotificationCenter.default.post(name: NSNotification.Name(rawValue: kLogoutNotification), object: nil)
    }
}

/// 退出登录
public func logout() {
    //deleteAccount()
    NotificationCenter.default.post(name: NSNotification.Name(rawValue: kLogoutNotification), object: nil)
}

// MARK: - denint
deinit {
    // 一定要记得移除通知
    NotificationCenter.default.removeObserver(self)
}

3.5 控制器

上面的四个层级代码说完了,控制器需要将各层级代码组合与用户交互,最后看一下控制器:

import UIKit

class DHLoginViewController: UIViewController, DHLoginViewDelegate {

    /// 登录UI
    private var loginView: DHLoginView!


    /// 初始并设置登录UI
    private func setLoginView() {
        loginView = DHLoginView(frame: self.view.frame)
        loginView.setBackgroundImage(imgName: "login_bg")
        loginView.delegate = self
        loginView.setLoginInfo(userName: DHLoginManager.shared.userName,
                               pwd: DHLoginManager.shared.password)
        self.view.addSubview(loginView)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = UIColor.white
        setLoginView()
    }


    // MARK: - DHLoginViewDelegate
    // 登录按钮响应方法
    func loginViewAction(userName: String, password: String) {
        if userName.lengthOfBytes(using: .utf8) == 0 && password.lengthOfBytes(using: .utf8) == 0 {
            MBProgressHUD.showTipMessage("请输入用户名和密码", inWindow: true)
            return
        }
        if userName.lengthOfBytes(using: .utf8) == 0 {
            MBProgressHUD.showTipMessage("请输入用户名", inWindow: true)
            return
        }
        if password.lengthOfBytes(using: .utf8) == 0 {
            MBProgressHUD.showTipMessage("请输入密码", inWindow: true)
            return
        }
        MBProgressHUD.showActivityMessage("", inWindow: true)
        DHLoginManager.shared.userName = userName
        DHLoginManager.shared.password = password
        DHLoginManager.shared.login()
    }
}

因为层级划分比较清晰,最后的控制器代码比较清晰,且代码量很少,有效减轻了控制器的压力!

以上就是根据安卓架构文章以及其它资料汇总后总结的一篇以登录为例的Demo,做此笔记记录。








附项目中总结封装的一些通用代码

↓↓↓↓↓↓↓↓↓↓↓↓↓↓ Alamofire二次封装代码: ↓↓↓↓↓↓↓↓↓↓↓↓↓↓

import UIKit
import Alamofire

enum MethodType {
    case get
    case post
}

// 必须要被强引用
private var _sessionManager: Alamofire.SessionManager!

class DHBaseNetwork {

    static var shared: Alamofire.SessionManager {

        let configuration = URLSessionConfiguration.default
        // 不配置该属性,不能同时进行多个网络请求
        configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
        configuration.timeoutIntervalForRequest = 5
        _sessionManager = Alamofire.SessionManager(configuration: configuration)
        return _sessionManager
    }


    // 定义回调参数
    typealias SuccessJSONHandle = (_ responseObject: [String:Any])->()
    typealias SuccessStringHandle = (_ responseObject: String)->()
    typealias SuccessDataHandle = (_ responseData: Data)->()
    typealias FailHandle = (_ error: Error)->()



    /// 请求方法 返回值JSON
    ///
    /// - Parameters:
    ///   - type: 请求类型
    ///   - URLString: 链接
    ///   - params: 参数
    ///   - success: 成功的回调
    ///   - failture: 失败的回调
    private class func requestDataToJSON(_ type: MethodType = .post, URLString: String, params: [String : Any]?, success: @escaping SuccessJSONHandle, fail: @escaping FailHandle) {

        // 1.获取类型
        let method = type == .get ? HTTPMethod.get : HTTPMethod.post

        // 2.发送网络请求
        self.shared.request(URLString, method: method, parameters: params)
            .responseJSON { (response) in
                switch response.result {
                case.success:
                    if let value = response.result.value as? [String : AnyObject] {
                        success(value)
                    }
                case .failure(let error):
                    fail(error)
                }
        }
    }


    /// 请求方法 返回值String
    private class func requestDataToString(_ type: MethodType = .post, URLString: String, params : [String : Any]?, success: @escaping SuccessStringHandle, fail: @escaping FailHandle) {
        // 1.获取类型
        let method = type == .get ? HTTPMethod.get : HTTPMethod.post
        // 2.发送网络请求
        DHBaseNetwork.shared.request(URLString, method: method, parameters: params)
            .responseString { (response) in
                switch response.result {
                case.success:
                    if let value = response.result.value {
                        success(value)
                    }
                case .failure(let error):
                    fail(error)
                }
        }
    }


    /// 请求方法 返回值Data
    private class func requestData(_ type: MethodType = .post, URLString: String, params : [String : Any]?, success: @escaping SuccessDataHandle, fail: @escaping FailHandle) {
        // 1.获取类型
        let method = type == .get ? HTTPMethod.get : HTTPMethod.post
        // 2.发送网络请求
        DHBaseNetwork.shared.request(URLString, method: method, parameters: params)
            .responseData { (response) in
                switch response.result {
                case.success:
                    if let value = response.result.value{
                        success(value)
                    }
                case .failure(let error):
                    fail(error)
                }
            }
    }
}

extension DHBaseNetwork {

    /// GET请求,返回JSON
    ///
    /// - Parameters:
    ///   - path: 请求路径
    ///   - params: 请求参数
    ///   - success: 请求成功回调
    ///   - fail: 请求错误回调
    public class func GET_JSON(path: String, params: [String : Any]?, success: @escaping SuccessJSONHandle, fail: @escaping FailHandle) {
        DHBaseNetwork.requestDataToJSON(.get, URLString: path, params: params, success: success, fail: fail)
    }



    /// GET请求,返回String
    ///
    /// - Parameters:
    ///   - path: 请求路径
    ///   - params: 请求参数
    ///   - success: 请求成功回调
    ///   - fail: 请求错误回调
    public class func GET_String(path: String, params: [String : Any]?, success: @escaping SuccessStringHandle, fail: @escaping FailHandle) {
        self.requestDataToString(.get, URLString: path, params: params, success: success, fail: fail)
    }



    /// GET请求,返回Data
    ///
    /// - Parameters:
    ///   - path: 请求路径
    ///   - params: 请求参数
    ///   - success: 请求成功回调
    ///   - fail: 请求错误回调
    public class func GET_Data(path: String, params: [String : Any]?, success: @escaping SuccessDataHandle, fail: @escaping FailHandle) {
        self.requestData(.get, URLString: path, params: params, success: success, fail: fail)
    }



    /// POST请求,返回JSON
    ///
    /// - Parameters:
    ///   - path: 请求路径
    ///   - params: 请求参数
    ///   - success: 请求成功回调
    ///   - fail: 请求错误回调
    public class func POST_JSON(path: String, params: [String : Any]?, success: @escaping SuccessJSONHandle, fail: @escaping FailHandle) {
        self.requestDataToJSON(.post, URLString: path, params: params, success: success, fail: fail)
    }



    /// POST请求,返回String
    ///
    /// - Parameters:
    ///   - path: 请求路径
    ///   - params: 请求参数
    ///   - success: 请求成功回调
    ///   - fail: 请求错误回调
    public class func POST_String(path: String, params: [String : Any]?, success: @escaping SuccessStringHandle, fail: @escaping FailHandle) {
        self.requestDataToString(.post, URLString: path, params: params, success: success, fail: fail)
    }



    /// POST请求,返回Data
    ///
    /// - Parameters:
    ///   - path: 请求路径
    ///   - params: 请求参数
    ///   - success: 请求成功回调
    ///   - fail: 请求错误回调
    public class func POST_Data(path: String, params: [String : Any]?, success: @escaping SuccessDataHandle, fail: @escaping FailHandle) {
        self.requestData(.post, URLString: path, params: params, success: success, fail: fail)
    }



    /// 取消所有请求
    public class func cancelAllQuestion() {
        DHBaseNetwork.shared.session.getAllTasks { (tasks) in
            for task in tasks {
                task.cancel()
            }
        }
    }


    /// 取消网络请求
    ///
    /// - Parameter URLStr: 请求URL字符串
    public class func cancelQuestion(withURL URLStr: String) {
        let url = URL(string: URLStr)
        let task = DHBaseNetwork.shared.session.dataTask(with: url!)
        task.cancel()
    }



    /// 中文参数的转为%形式
    public class func percentPath(WithPath path : NSString, params : NSDictionary?) -> String {
        let percentPath = NSMutableString(string: path)
        if params != nil {
            var keys = params!.allKeys
            let count = keys.count
            for i in 0..<count {
                let key : NSString = keys[i] as! NSString
                let value : NSString = params![key] as! NSString
                if i == 0 {
                    percentPath.appendFormat("?%@=%@", key, value)
                } else {
                    percentPath.appendFormat("&%@=%@", key, value)
                }
            }
        }
        //return percentPath.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding)!
        return percentPath.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed)!
    }
}

↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 登录UI封装视图: ↓↓↓↓↓↓↓↓↓↓↓↓↓↓

import UIKit

protocol DHLoginViewDelegate {
    func loginViewAction(userName: String, password: String)
}

class DHLoginView: UIView, UITextFieldDelegate {

    var delegate: DHLoginViewDelegate?

    /// 背景视图
    private lazy var bgImgView: UIImageView = {
        let bgIV = UIImageView()
        bgIV.image = UIImage(named: "login_bg")
        return bgIV
    }()

    /// 账号密码容器视图
    private lazy var contentView: UIView = {
        let conView = UIView()

        conView.backgroundColor = UIColor.white
        conView.layer.cornerRadius = 5
        conView.clipsToBounds = true
        return conView
    }()

    /// 账号TF
    private lazy var userTF: UITextField = {
        let tf = UITextField()
        tf.delegate = self
        tf.placeholder = "账号"
        tf.clearButtonMode = .whileEditing
        tf.autocorrectionType = .no
        // 首字母大写
        tf.autocapitalizationType = .none
        tf.leftView = UIImageView(image: UIImage(named: "login_yh"))
        tf.leftViewMode = .always
        return tf
    }()

    /// 密码TF
    private lazy var pwdTF: UITextField = {
        let tf = UITextField()
        tf.delegate = self
        tf.placeholder = "密码"
        tf.clearButtonMode = .whileEditing
        tf.autocorrectionType = .no
        tf.isSecureTextEntry = true
        // 首字母大写
        tf.autocapitalizationType = .none
        tf.leftView = UIImageView(image: UIImage(named: "login_pd"))
        tf.leftViewMode = .always
        return tf
    }()

    /// 装饰分割线
    private lazy var splitLine: UIView = {
        let line = UIView()
        line.backgroundColor = UIColor.colorWithCustom(242, g: 242, b: 242)
        return line
    }()

    /// 登录按钮
    private lazy var loginBtn: UIButton = {
        let btn = UIButton(type: .custom)
        btn.setTitle("登     录", for: .normal)
        btn.setTitle("登     录       中...", for: .selected)
        btn.setTitleColor(kThemeColor, for: .normal)
        btn.setTitleColor(UIColor.red, for: .selected)
        btn.backgroundColor = UIColor.white
        btn.layer.cornerRadius = 5
        btn.clipsToBounds = true
        btn.addTarget(self, action: #selector(loginAction(sender:)), for: .touchUpInside)
        return btn
    }()

    /// 应用标题
    private lazy var titleLb: UILabel = {
        let label = UILabel()
        label.text = "东华软件管理部"
        label.textAlignment = .center
        label.font = UIFont.boldSystemFont(ofSize: 35)
        label.textColor = UIColor.white
        label.shadowColor = UIColor.black
        label.shadowOffset = CGSize(width: 1.0, height: 1.0)
        return label
    }()

    /// 键盘动画时长
    private var duration: TimeInterval = 0

    private func setupSubviews() {
        // 背景图片
        bgImgView.frame = self.frame
        self.addSubview(self.bgImgView)

        // 应用标题
        titleLb.frame = CGRect(x: 0, y: 50, width: SCREEN_WIDTH, height: 100)
        self.addSubview(titleLb)

        // 容器视图
        contentView.frame = CGRect(x: 30, y: 170, width: SCREEN_WIDTH-60, height: 160)
        self.addSubview(contentView)

        // 账号输入框
        userTF.frame = CGRect(x: 20, y: 0, width: SCREEN_WIDTH-60-40, height: 80)
        self.contentView.addSubview(userTF)
        // 密码输入框
        pwdTF.frame = CGRect(x: 20, y: 80, width: SCREEN_WIDTH-60-40, height: 80)
        self.contentView.addSubview(pwdTF)

        // 分隔线
        splitLine.frame = CGRect(x: 20, y: 79, width: SCREEN_WIDTH-60-40, height: 2)
        self.contentView.addSubview(splitLine)

        // 登录按钮
        loginBtn.frame = CGRect(x: 30, y: 400, width: SCREEN_WIDTH-60, height: 50)
        self.addSubview(loginBtn)
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = UIColor.white
        self.setupSubviews()

        let tapGes = UITapGestureRecognizer(target: self, action: #selector(closeKeyboard))
        self.addGestureRecognizer(tapGes)
        self.addNotificationWithKeyboard()

//        NotificationCenter.default.addObserver(self, selector: #selector(loginFail), name: NSNotification.Name(rawValue: kLogoutNotification), object: nil)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    // MARK: - 公开的设置方法

    /// 设置登录的背景图片
    ///
    /// - Parameter imgName: 背景图片名
    public func setBackgroundImage(imgName: String) {
        self.bgImgView.image = UIImage(named: imgName)
    }

    /// 设置显示的账号和密码
    ///
    /// - Parameters:
    ///   - userName: 账号
    ///   - pwd: 密码
    public func setLoginInfo(userName: String, pwd: String) {
        self.userTF.text = userName
        self.pwdTF.text = pwd
    }


    // MARK: - 内部逻辑方法

    /// 关闭键盘
    @objc private func closeKeyboard() {
        self.endEditing(true)

        UIView.animate(withDuration: self.duration, animations: {
            [unowned self] in
            self.frame.origin.y = 0
        })
    }

    /// 添加对键盘的监听
    private func addNotificationWithKeyboard() {
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChangeFrame(note:)), name: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil)
    }

    //监听键盘的事件
    @objc private func keyboardWillChangeFrame(note: Notification) {
        // 1.获取动画执行的时间
        duration = note.userInfo?[UIKeyboardAnimationDurationUserInfoKey] as! TimeInterval
        // 2.获取键盘最终 Y值
        let endFrame = (note.userInfo?[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
        let keyboardY = endFrame.origin.y

        if keyboardY < SCREEN_HEIGHT {
            // 获取contentView下边缘Y值
            let conBottomY = contentView.frame.origin.y + contentView.frame.size.height
            let offsetY = keyboardY - conBottomY
            // 键盘没有挡住contView不用执行动画
            if offsetY <= 0 {
                UIView.animate(withDuration: duration) {
                    [weak self] in
                    self?.contentView.frame.origin.y += offsetY
                }
            }
        } else {
            // 关闭键盘
            UIView.animate(withDuration: duration) {
                [weak self] in
                self?.contentView.frame.origin.y = 170
            }
        }
    }

    /// 登录操作
    @objc private func loginAction(sender: UIButton) {
        if (delegate != nil) {
            delegate?.loginViewAction(userName: userTF.text!, password: pwdTF.text!)
        }
    }


    // MARK: - UITextFieldDelegate
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        if string.lengthOfBytes(using: .utf8) == 0 {
            return true
        }
        if textField == userTF {
            // 用户名最大16位长度
            if ((textField.text!+string).lengthOfBytes(using: .utf8)) > 16 {
                MBProgressHUD.showTipMessage("用户名长度超出限制", in: self)
                return false
            }
            return true
        }
        else if textField == pwdTF {
            // 密码最大16位长度
            if ((textField.text!+string).lengthOfBytes(using: .utf8)) > 16 {
                MBProgressHUD.showTipMessage("密码长度超出限制", in: self)
                return false
            }
            return true
        }

        return true
    }

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        self.endEditing(true)
        return true
    }


    // MARK: - deinit
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}

猜你喜欢

转载自blog.csdn.net/kangpengpeng1/article/details/80741459