本帖最后由 Ding 于 2016-10-25 13:28 编辑

接触了自动布局的第三方库Masonry、Core BlueTooth的封装库BabyBlueTooth等,喜欢那些小圆点链式语法。
OC世界的方括号,嵌套多了就会像噩梦,适当地用用链式语法还是不错的。Swift本来就是点语法,但如果在适当的时候来点链式也能增加代码可读性。


有趣的东西!就以系统警示视图(有的框架里叫对话框)的使用为例探索一下。

目前开发的应用大多最低支持到iOS8,同时为了我们的探索更简单,就舍弃UIAlertView和UIActionSheet,只以UIAlertController为素材做我们的菜。(如果你的应用需要兼容iOS8之前的版本,也可以对UIAlertView和UIActionSheet做封装,让它们支持链式语法,不过要处理代理为闭包,稍微麻烦点。)

先来看UIAlertController的基本使用方式。假设我们要在某个UIViewController里展示一个这样的警示框:

探索之旅:链式语法

探索之旅:链式语法 - 敏捷大拇指 - 探索之旅:链式语法


就需要这样写了:
[Swift] 纯文本查看 复制代码
let alert = UIAlertController(title: "Title", message: "message", preferredStyle: .alert)
        alert.addTextField { textField in
            textField.placeholder = "Username"
        }
        alert.addTextField { textField in
            textField.placeholder = "Password"
            textField.isSecureTextEntry = true
        }
        let loginAction = UIAlertAction(title: "Login", style: .default) { action in
            if let textFields = alert.textFields {
                print("Username:\(textFields[0].text!)\nPassword:\(textFields[1].text!)")
            }
        }
        let cancelAction = UIAlertAction(title: "cacel", style: .cancel, handler: nil)
        alert.addAction(loginAction)
        alert.addAction(cancelAction)
        self.present(alert, animated: true, completion: nil)


用链式语法封装后呢?可以这样写:
[Swift] 纯文本查看 复制代码
self.alert(title: "Title", message: "message")
            .textField(configuration: { textField in
                textField.placeholder = "Username"
            })
            .textField(configuration: { textField in
                textField.placeholder = "Password"
                textField.isSecureTextEntry = true
            })
            .normalButton(title: "Login") { alert in
                if let textFields = alert.textFields {
                    print("Username:\(textFields[0].text!)\nPassword:\(textFields[1].text!)")
                }
            }
            .cancleButton(title: "cancle")
            .show(animated: true)


这就是我们的目标。

大体思路:我们写个UIViewController的扩展,用关联对象的方式添加几个属性,比如alertTitle、alertMessage甚至一个UIAlertController类型的属性,但是这样不很美。是的,有更好的方法。我们干脆写一个类,就叫ChainableAlert,这个类有那些属性,并支持链式语法,然后在UIViewControll的扩展里简单使用这个类即可。这样写还有个小小的好处:如果在一个非viewController的地方要展示警示框,可以直接用这个ChainableAlert来做(其实就是用UIApplication.shared.keyWindow?.rootViewController来做展示用的controller)。


在OC里实现点语法,要用上block(类似Swift里的闭包),但是在Swift里,简单多了。怎么做?不妨先闭上眼睛想一想,顺便休息下~

我们可以让方法返回实例自身!

其实到这里,本次探索已经在理论上结束了,不过我们还是走完实际的路吧。燃烧一把:
[Swift] 纯文本查看 复制代码
public typealias AlertButtonAction = (ChainableAlert) -> Void
public typealias AlertTextFieldConfigurationHandler = (UITextField) -> Void

@available (iOS 8, *)
public class ChainableAlert {
    
    // Mark: init
    
    /**
     Create an alert
     */
    public static func alert(title: String? = nil, message: String? = nil) -> ChainableAlert {
        return ChainableAlert(title: title, message: message, style: .alert)
    }
    /**
     Create an action sheet
     */
    public static func actionSheet(title: String? = nil, message: String? = nil) -> ChainableAlert {
        return ChainableAlert(title: title, message: message, style: .actionSheet)
    }
    
    // Mark: add buttons
    
    /**
     Add a normal button to the alert
     */
    public func normalButton(title: String, handler: AlertButtonAction? = nil) -> ChainableAlert {
        let entity = AlertButtonEntity(title: title, action: handler == nil ? {_ in} : handler!)
        if normalEntities == nil {
            normalEntities = []
        }
        normalEntities?.append(entity)
        return self
    }
    
    /**
     Add a destructive button to the alert
     */
    public func destructiveButton(title: String, handler: AlertButtonAction? = nil) -> ChainableAlert {
        let entity = AlertButtonEntity(title: title, action: handler == nil ? {_ in} : handler!)
        if destructiveEntities == nil {
            destructiveEntities = []
        }
        destructiveEntities?.append(entity)
        return self
    }
    
    /**
     Add a cancel button to the alert, the most number of cancel button is 1
     */
    public func cancleButton(title: String, handler: AlertButtonAction? = nil) -> ChainableAlert {
        let entity = AlertButtonEntity(title: title, action: handler == nil ? {_ in} : handler!)
        cancleEntity = entity
        return self
    }
    
    /**
     Add a textField to the alert, if is action sheet, no use.
     */
    public func textField(configuration: AlertTextFieldConfigurationHandler? = nil) -> ChainableAlert {
        guard style == .alert else {
            return self
        }
        let handler = configuration == nil ? {_ in } : configuration!
        if textHandlers == nil {
            textHandlers = []
        }
        textHandlers?.append(handler)
        return self
    }
    
    /// textFields added to the alert.
    public var textFields: [UITextField]?
    
    // Mark: show
    
    /**
     Show the alert
     - parameter viewController: We will use it to present
     - parameter fromPosition:   If is action sheet, and device is iPad, we can set the source point for the popover controller
     */
    public func show(viewController: UIViewController? = nil, animated:Bool, fromPosition:(x: CGFloat, y: CGFloat)? = nil, completion: (() -> Void)? = nil) {
        let alertStyle: UIAlertControllerStyle = style == .alert ? .alert : .actionSheet
        let alertController = UIAlertController(title: title, message: message, preferredStyle: alertStyle)
        
        if let entities = normalEntities {
            for entity in entities {
                let action = UIAlertAction(title: entity.title, style: .default) { action in
                    let buttonAction = entity.action
                    buttonAction(self)
                }
                alertController.addAction(action)
            }
        }
        
        if let entities = destructiveEntities {
            for entity in entities {
                let action = UIAlertAction(title: entity.title, style: .destructive) { action in
                    let buttonAction = entity.action
                    buttonAction(self)
                }
                alertController.addAction(action)
            }
        }
        
        if let entity = cancleEntity {
            let action = UIAlertAction(title: entity.title, style: .cancel) { action in
                let buttonAction = entity.action
                buttonAction(self)
            }
            alertController.addAction(action)
        }
        
        if let textHandlers = textHandlers {
            for handler in textHandlers {
                alertController.addTextField(configurationHandler: handler)
            }
            textFields = alertController.textFields
        }
        
        func showWithViewController(controller: UIViewController) {
            if let popoverController = alertController.popoverPresentationController {
                popoverController.sourceView = controller.view
                
                if let fromPosition = fromPosition {
                    popoverController.sourceRect = CGRect(x: fromPosition.x, y: fromPosition.y, width: 0, height: 0)
                } else {
                    let size = controller.view.bounds.size
                    popoverController.sourceRect = CGRect(x: size.width/2, y: size.height - 2, width: 0, height: 2)
                }
            }
            controller.present(alertController, animated: animated, completion: completion)
        }
        
        if let controller = viewController {
            showWithViewController(controller: controller)
        } else if let controller = UIApplication.shared.keyWindow?.rootViewController {
            showWithViewController(controller: controller)
        }
    }
    
    // Mark: private
    
    fileprivate var title: String?
    fileprivate var message: String?
    fileprivate var style: AlertStyle = .alert
    
    fileprivate var normalEntities: [AlertButtonEntity]?
    fileprivate var destructiveEntities: [AlertButtonEntity]?
    fileprivate var cancleEntity: AlertButtonEntity?
    fileprivate var textHandlers: [AlertTextFieldConfigurationHandler]?
    
    private init(title: String?, message: String?, style: AlertStyle) {
        self.title = title
        self.message = message
        self.style = style
    }
    
}

// Mark: - private

private extension ChainableAlert {
    
    enum AlertStyle : Int {
        case alert
        case actionSheet
    }
    
    struct AlertButtonEntity {
        var title: String
        var action: AlertButtonAction
    }
}


在UIViewController的扩展文件里:
[Swift] 纯文本查看 复制代码
@available (iOS 8, *)
public extension UIViewController {
    
    /**
     Create an alert
     */
    public func alert(title: String? = nil, message: String? = nil) -> UIViewController {
        chainableAlert = ChainableAlert.alert(title: title, message: message)
        return self
    }
    /**
     Create an action sheet
     */
    public func actionSheet(title: String? = nil, message: String? = nil) -> UIViewController {
        chainableAlert = ChainableAlert.actionSheet(title: title, message: message)
        return self
    }
    
    // Mark: add buttons
    
    /**
     Add a normal button to the alert
     */
    public func normalButton(title: String, handler: AlertButtonAction? = nil) -> UIViewController {
        let _ = chainableAlert?.normalButton(title: title, handler: handler)
        return self
    }
    
    /**
     Add a destructive button to the alert
     */
    public func destructiveButton(title: String, handler: AlertButtonAction? = nil) -> UIViewController {
        let _ = chainableAlert?.destructiveButton(title: title, handler: handler)
        return self
    }
    
    /**
     Add a cancel button to the alert, the most number of cancel button is 1
     */
    public func cancleButton(title: String, handler: AlertButtonAction? = nil) -> UIViewController {
        let _ = chainableAlert?.cancleButton(title: title, handler: handler)
        return self
    }
    
    /**
     Add a textField to the alert, if is action sheet, no use.
     */
    public func textField(configuration: AlertTextFieldConfigurationHandler? = nil) -> UIViewController {
        let _ = chainableAlert?.textField(configuration: configuration)
        return self
    }
    
    // Mark: show
    
    /**
     Show the alert
     - parameter fromPosition:   If is action sheet, and device is iPad, we can set the source point for the popover controller
     */
    public func show(animated: Bool, fromPosition: (x: CGFloat, y: CGFloat)? = nil, completion: (() -> Void)? = nil) {
        if let alert = chainableAlert {
            alert.show(viewController: self, animated: animated, fromPosition: fromPosition) {
                completion?()
            }
        }
    }
}

private extension UIViewController {
    struct AssociatedKeys {
        static var chainableAlertKey = "chainableAlertKey"
    }
    var chainableAlert: ChainableAlert? {
        get {
            return (objc_getAssociatedObject(self, &AssociatedKeys.chainableAlertKey) as? ChainableAlert)
        }
        set {
            objc_setAssociatedObject(self, &AssociatedKeys.chainableAlertKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
}



最后还是说一句:爱起来,用起来,做起来~


写着写着,慢慢就多起来,应阿牛哥之请,弄了个海淘贴:探索之旅。可以去订阅。