NotificationCenter Protocol 以及伪 ViewModel 实战

July 01, 2017

前言

开发过程中的小总结.

NotificationCenter Protocol

NotificationCenter Protocol 乍一看很别扭, 这明明是两种设计模式. 实际上, 这里是将 观察者模式代理模式 融合到了一起, 将观察者模式代理化了.

为什么要这样做?

保证观察对象的一致性

哪些类是观察者, 可以通过协议一目了然. 避免之前多种多样的发送通知的方式.

避免通知名称冲突

如果随意起名, 可能会导致两个不同的观察对象拥有相同的通知名, 后期容易通知混乱, 难以维护.

避免使用字符串作为通知名称

这是比较 Hard Code 的方式, 在开发过程中难以用 Xcode 自动补齐.

那么, 如何进行改造 NotificationCenter, 国外这篇博客写的非常好, 而且 SwiftGG 也翻译了这篇文章, 我就不再写一遍了, 大家可以看一看作者的 playground.

代码如下:

// MARK: - Protocol

public protocol Notifier {
    associatedtype Notification: RawRepresentable
}

public extension Notifier where Notification.RawValue == String {
    
    // MARK: - Static Computed Variables
    
    private static func nameFor(notification: Notification) -> String {
        return "\(self).\(notification.rawValue)"
    }
    
    
    // MARK: - Instance Methods
    
    // Pot
    
    func postNotification(notification: Notification, object: AnyObject? = nil) {
        Self.postNotification(notification, object: object)
    }
    
    func postNotification(notification: Notification, object: AnyObject? = nil, userInfo: [String : AnyObject]? = nil) {
        Self.postNotification(notification, object: object, userInfo: userInfo)
    }
    
    
    // MARK: - Static Function
    
    // Post
    
    static func postNotification(_ notification: Notification, object: AnyObject? = nil, userInfo: [String : AnyObject]? = nil) {
        let name = nameFor(notification: notification)
        
        NotificationCenter.default
            .post(name: NSNotification.Name(rawValue: name), object: object, userInfo: userInfo)
    }
    
    // Add
    
    static func addObserver(observer: AnyObject, selector: Selector, notification: Notification) {
        let name = nameFor(notification: notification)
        
        NotificationCenter.default
            .addObserver(observer, selector: selector, name: NSNotification.Name(rawValue: name), object: nil)
    }
    
    // Remove
    
    static func removeObserver(observer: AnyObject, notification: Notification, object: AnyObject? = nil) {
        let name = nameFor(notification: notification)
        
        NotificationCenter.default
            .removeObserver(observer, name: NSNotification.Name(rawValue: name), object: object)
    }
}

使用示例:

// MARK: - Example

class Barista : Notifier {
    enum Notification : String {
        case makeCoffee
    }
}

extension Selector {
    static let makeCoffeeNotification = #selector(Customer.drink(notification:))
}

class Customer {
    @objc func drink(notification: NSNotification) {
        print("Mmm... Coffee")
    }
}


let customer = Customer()

Barista.addObserver(observer: customer, selector: .makeCoffeeNotification, notification: .makeCoffee)

Barista.postNotification(.makeCoffee)
// prints: Mmm... Coffee

Barista.removeObserver(observer: customer, notification: .makeCoffee)

NotificationCenter Protocol 实战

一个很简单的例子. 用户在侧滑菜单处, 点击进入登录页面, 登录成功后, 登录页面自行 dismiss, 并利用 token 获取用户信息, 信息获取后, 自动刷新侧滑菜单的 UI.

lg-01

简单的分析一下:

  • 如果你只是在 LoginViewController(登录) 里利用 Alamofire 发送请求, 得到数据后, 再次请求用户信息, 并 dismiss 页面, 那么 LeftViewController(侧滑菜单) 无法知道用户信息何时已获取并刷新 UI. 所以, 最好是用 NotificationCenter 来做.
  • 跨界面传数据有很多种方式, 我考虑的做法是, 将 User 相关的数据抽象出一个单例来管理. 好处不仅在于, 方便请求数据, 存储数据等, 还要实现上面博客所说的 Notifier 协议, 向它的订阅者推送通知.

Show me the code:

每个业务操作对应了两种通知类型, didXXX 成功通知和 didXXXFailure 失败通知, 如下:

// UserManager.swift
public class UserManager: Notifier {
    
    public static let shared: UserManager = UserManager()
    
    public enum Notification : String {
        case didGetMessage
        case didGetMessageFailure
        case didSignup
        case didSignupFailure
        case didLogin
        case didLoginFailure
        case didGetUserInfo
        case didGetUserInfoFailure
        ...
    }
}

对于一种规范的后台接口, 处理响应数据的代码会有很多重复. 比如, 网络问题发送失败通知, JSON解析不对发送失败通知, 状态码不对发送失败通知, 打印失败/成功的日志, 成功操作后推送成功通知等等. 所以我把重复的操作用统一的方法 handleResult 来做, 如下:

// UserManager.swift
    private func handleResult(_ action: Action, _ response: DataResponse<Any>, completionHandler: (JSON) -> ()) {
        switch response.result {
        case .success:
            guard let value = response.result.value else {
                log("response.result.value is nil", .error)
                return
            }
            let json = JSON(value)
            guard let status = json["code"].int else { return }
            guard status == 0 else {
                log(json, .error)
                
                let (_, failureNoti) = actionDict[action]!
                UserManager.postNotification(failureNoti)
                
                return
            }
            let (successNoti, _) = actionDict[action]!
            completionHandler(json)
            UserManager.postNotification(successNoti)
            return
        case .failure(let error):
            log(error, .error)
            let (_, failureNoti) = actionDict[action]!
            UserManager.postNotification(failureNoti)
            return
        }
    }

那么这个 Action 是我自己定义的, 如下:

// UserManager.swift
    private enum Action {
        case getMessage
        case signUp
        case logIn
        case getUserInfo
        case getAvatar
    }
    
    private let actionDict: [Action: (Notification, Notification)] =
        [.getMessage: (.didGetMessage, .didGetMessageFailure),
         .signUp: (.didSignup, .didSignupFailure),
         .logIn: (.didLogin, .didLoginFailure),
         .getUserInfo: (.didGetUserInfo, .didGetUserInfoFailure),
         .getAvatar: (.didGetUserAvatar, .didGetUserAvatarFailure)]

定义完之后, 请求以及对响应数据的操作变得相当简洁. 例如, 注册时, 将 Action 对应的 .signUp, 响应数据 response 和操作闭包传给 handleResult 即可. 不管成功与否, 由 handleResult 来推送相应的通知, 并处理 json 数据. 例如:

// UserManager.swift
    func signup(username: String, password: String, name: String, email: String, photo: String) {
        Alamofire.request(Router.signUp(username, password, name, email, photo)).responseJSON { response in
            self.handleResult(.signUp, response, completionHandler: { (json) in
                log(json, .json)
            })
        }
    }
    
    func login(username: String, password: String) {
        Alamofire.request(Router.logIn(username, password)).responseJSON { response in
            self.handleResult(.logIn, response, completionHandler: { (json) in
                log(json, .json)
                
                self.token = json["result"].string!
                log(self.token, .json)
                
                self.isLogIn = true
            })
        }
    }

这样的话, UserManager 作为单例, 管理着整个应用与 User 相关的数据和操作. 其他 ViewController 在注册 UserManager 的通知后, 只需要调用其方法, 等待通知就行了.

在相应的 ViewController 里面注册和取消通知, 如下:

// LoginViewController.swift
override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        UserManager.addObserver(observer: self, selector: .userDidLogin, notification: .didLogin)
        UserManager.addObserver(observer: self, selector: .userDidLoginFailure, notification: .didLoginFailure)
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        UserManager.removeObserver(observer: self, notification: .didLogin)
        UserManager.removeObserver(observer: self, notification: .didLoginFailure)
    }

上面的方法已经对 Selector 进行了扩展. 这样做在使用的时候更简洁, 同样是该作者的一篇关于 Selector 语法糖的文章 Swift: Selector Syntax Sugar, 扩展如下:

extension Selector {
    // Notifier Action
    static let userDidLogin = #selector(LoginViewController.userDidLogin(notification:))
    static let userDidLoginFailure = #selector(LoginViewController.userDidLoginFailure(notification:))
}

ViewController 收到通知后, 对 View 进行改变, 例如:

// LoginViewController.swift
    func userDidLogin(notification: NSNotification) {
        loginButton.isActive = false
        self.noticeSuccess("登录成功")
        DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute: {
            self.dismiss(animated: true, completion: nil)
        })
    }
    
    func userDidLoginFailure(notification: NSNotification) {
        loginButton.isActive = false
        self.noticeError("登录失败")
    }

回顾

当时写的时候, 没有接触过 MVVM 的概念, 自己完全为了业务逻辑抽象出来一层瞎写的. 然而后来发现, 有那么一点 ViewModel 的味道.

对于 MVVM, 在 ViewController 里面调用 ViewModel 的方法, ViewModel 来和 Model 通信, 通过 block 回调或者 ReactiveCocoa 来改变 ViewController 里的 View.

对于本实例, 在 ViewController 里面调用 UserManager 的方法, UserManager 负责发网络请求获取数据 Model, 通过观察者模式来通知 ViewController 改变 View.

毕竟 ReactiveCocoa 是用观察者模式来实现 ViewModel 的 View 绑定.

这么做, 其实是为了给 ViewController 瘦身, 避免 MVC 的 “Massive View Controller” 缺点.

有待改进的地方

  • didXXXFailure 失败通知无法获知失败原因. 可以用通知中的 userInfo 来传递.
  • 太麻烦. 写这么一个逻辑, 要在好几个文件里同时添加代码, 后期嫌麻烦. 可以将 didXXXdidXXXFailure 合成一个, 用 userInfo 来传递是否有错误.
  • 或者不这么做了, 用 MVVM 来做, 用 Moya + RxSwift 来做.

参考文章: SwiftGG

You can contact me on Twitter @Ji4n1ng

Email me if you need anything.
contact@jianing.wang

All background images are from DesignCode. iOS app and this website are made entirely by @Ji4n1ng. © 2019