// // PopupViewController.swift // JiuLaiBao // // Created by Dan Jiang on 2018/11/13. // Copyright © 2018 GuoXiaoMei. All rights reserved. // import UIKit import SnapKit public struct PopupViewConfig { static public var width: CGFloat = 280 static public var vMargin: CGFloat = 34 static public var hMargin: CGFloat = 20 static public var vSpace: CGFloat = 16 static public var buttonHeight: CGFloat = 50 static public var buttonBorderColor = UIColor.gray static public var closeButtonVMargin: CGFloat = 46 static public var closeButtonInset: UIEdgeInsets = .init(top: 12, left: 12, bottom: 12, right: 12) static public var headlineColor = UIColor.black static public var headlineFont = UIFont.boldSystemFont(ofSize: 17) static public var messageColor = UIColor.black static public var messageFont = UIFont.systemFont(ofSize: 17) static public var confirmColor = UIColor.black static public var confirmFont = UIFont.boldSystemFont(ofSize: 16) static public var cancelColor = UIColor.gray static public var cancelFont = UIFont.boldSystemFont(ofSize: 16) static public var textFieldHeight: CGFloat = 40 static public var textFieldBorderColor = UIColor.gray static public var textFieldTextColor = UIColor.black static public var textFieldTextFont = UIFont.systemFont(ofSize: 14) static public var textFieldPlaceholderColor = UIColor.gray static public var textFieldPlaceholderFont = UIFont.systemFont(ofSize: 14) static public var textFieldTipsColor = UIColor.red static public var textFieldTipsFont = UIFont.systemFont(ofSize: 11) static public var textFieldTipsHMargin: CGFloat = 30 static public var textFieldTipsVMargin: CGFloat = 6 } public class PopupAction { public let image: UIImage? public let attributedTitle: NSAttributedString? public let handler: ((PopupAction) -> Void)? public let autoDismiss: Bool public let position: Position public enum Style { case confirm case cancel } public enum Position { case bottom case topRight } convenience public init(title: String, style: Style, autoDismiss: Bool = true, handler: ((PopupAction) -> Void)? = nil) { let attributedTitle: NSAttributedString? switch style { case .confirm: attributedTitle = NSAttributedString(string: title, attributes: [.font: PopupViewConfig.confirmFont, .foregroundColor: PopupViewConfig.confirmColor]) case .cancel: attributedTitle = NSAttributedString(string: title, attributes: [.font: PopupViewConfig.cancelFont, .foregroundColor: PopupViewConfig.cancelColor]) } self.init(attributedTitle: attributedTitle, autoDismiss: autoDismiss, handler: handler) } convenience public init(image: UIImage?, autoDismiss: Bool = true, handler: ((PopupAction) -> Void)? = nil) { self.init(attributedTitle: nil, image: image, position: .topRight, autoDismiss: autoDismiss, handler: handler) } public init(attributedTitle: NSAttributedString?, image: UIImage? = nil, position: Position = .bottom, autoDismiss: Bool = true, handler: ((PopupAction) -> Void)? = nil) { self.attributedTitle = attributedTitle self.image = image self.position = position self.autoDismiss = autoDismiss self.handler = handler } } open class PopupViewController: UIViewController { public enum Style { case alert case sheet } public enum OverlayStyle { case transparent case dark case extraLightBlur case lightBlur case darkBlur } public var style: Style { return inStyle } public var overlayStyle: OverlayStyle { return inOverlayStyle } public var headline: String? { get { return headlineLabel?.text } set { headlineLabel?.text = newValue } } public var attributedHeadline: NSAttributedString? { get { return headlineLabel?.attributedText } set { headlineLabel?.attributedText = newValue } } public var message: String? { get { return messageLabel?.text } set { messageLabel?.text = newValue } } public var attributedMessage: NSAttributedString? { get { return messageLabel?.attributedText } set { messageLabel?.attributedText = newValue } } public var textField: UITextField? { return inTextField } public var customView: UIView? { return inCustomView } public var actions: [PopupAction] { return inActions } public var presentAnimator: UIViewControllerAnimatedTransitioning? public var dimsissAnimator: UIViewControllerAnimatedTransitioning? public var completionCallback: (() -> Void)? public var canDismissOnOverlay: Bool? let inStyle: Style let inOverlayStyle: OverlayStyle var inCanDismissOnOverlay = true var overlayView: UIView! var contentView: UIView! var headlineLabel: UILabel? var messageLabel: UILabel? var tipsLabel: UILabel? var inTextField: UITextField? var inCustomView: UIView? var actionButtons: [UIButton] = [] var inActions: [PopupAction] = [] required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } public init(style: Style, overlayStyle: OverlayStyle = .dark) { self.inStyle = style self.inOverlayStyle = overlayStyle super.init(nibName: nil, bundle: nil) modalPresentationStyle = .overFullScreen transitioningDelegate = self } deinit { print("\(self) deinit") } public func addHeadline(_ headline: String) { let attributedHeadline = NSAttributedString(string: headline, attributes: [.font: PopupViewConfig.headlineFont, .foregroundColor: PopupViewConfig.headlineColor]) addAttributedHeadline(attributedHeadline) } public func addAttributedHeadline(_ headline: NSAttributedString?) { if headlineLabel == nil { let label = UILabel() label.attributedText = headline headlineLabel = label } } public func addMessage(_ message: String, alignment: NSTextAlignment = .left) { let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineSpacing = 6 paragraphStyle.alignment = alignment let attributedMessage = NSAttributedString(string: message, attributes: [.font: PopupViewConfig.messageFont, .foregroundColor: PopupViewConfig.messageColor, .paragraphStyle: paragraphStyle]) addAttributedMessage(attributedMessage) } public func addAttributedMessage(_ message: NSAttributedString?) { if messageLabel == nil { let label = UILabel() label.attributedText = message label.numberOfLines = 0 label.lineBreakMode = .byCharWrapping messageLabel = label } } public func addTextField(configurationHandler: ((UITextField) -> Void)? = nil) { if tipsLabel == nil { tipsLabel = UILabel() tipsLabel?.textAlignment = .center } if inTextField == nil { let textField = UITextField() textField.layer.borderColor = PopupViewConfig.textFieldBorderColor.cgColor textField.layer.borderWidth = 0.5 textField.textColor = PopupViewConfig.textFieldTextColor textField.font = PopupViewConfig.textFieldTextFont let leftView = UIView(frame: .init(x: 0, y: 0, width: 10, height: PopupViewConfig.textFieldHeight)) textField.leftView = leftView textField.leftViewMode = .always inTextField = textField configurationHandler?(textField) } } public func setTextFieldPlaceholder(_ placehoder: String?) { let attributedPlaceholder = NSAttributedString(string: placehoder ?? "", font: PopupViewConfig.textFieldPlaceholderFont, color: PopupViewConfig.textFieldPlaceholderColor) setTextFieldAttributedPlaceholder(attributedPlaceholder) } public func setTextFieldAttributedPlaceholder(_ placehoder: NSAttributedString?) { inTextField?.attributedPlaceholder = placehoder } public func setTextFieldTips(_ tips: String?) { let attributedTips = NSAttributedString(string: tips ?? "", font: PopupViewConfig.textFieldTipsFont, color: PopupViewConfig.textFieldTipsColor) setTextFieldAttributedTips(attributedTips) } public func setTextFieldAttributedTips(_ tips: NSAttributedString?) { tipsLabel?.attributedText = tips } public func addCustomView(configurationHandler: ((UIView) -> Void)) { if inCustomView == nil { let customView = UIView() inCustomView = customView configurationHandler(customView) } } public func addAction(_ action: PopupAction) { inActions.append(action) } public func hide(callback: (() -> Void)? = nil) { if let callback = callback { presentingViewController?.dismiss(animated: true, completion: { callback() if let completion = self.completionCallback { completion() } }) } else { presentingViewController?.dismiss(animated: true, completion: completionCallback) } } open override func loadView() { super.loadView() } open override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .clear if let canDismissOnOverlay = canDismissOnOverlay { inCanDismissOnOverlay = canDismissOnOverlay } else { switch style { case .alert: inCanDismissOnOverlay = false case .sheet: inCanDismissOnOverlay = true } } layoutOverlayView() layoutContentView() } func layoutOverlayView() { switch overlayStyle { case .transparent: overlayView = UIView() overlayView.backgroundColor = UIColor(white: 0, alpha: 0) case .dark: overlayView = UIView() overlayView.backgroundColor = UIColor(white: 0, alpha: 0.65) case .extraLightBlur: let blurEffect = UIBlurEffect(style: .extraLight) overlayView = UIVisualEffectView(effect: blurEffect) case .lightBlur: let blurEffect = UIBlurEffect(style: .light) overlayView = UIVisualEffectView(effect: blurEffect) case .darkBlur: let blurEffect = UIBlurEffect(style: .dark) overlayView = UIVisualEffectView(effect: blurEffect) } if inCanDismissOnOverlay { overlayView.isUserInteractionEnabled = true overlayView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onDismiss))) } view.addSubview(overlayView) overlayView.snp.makeConstraints { make in make.edges.equalToSuperview() } } func layoutContentView() { contentView = UIView() view.addSubview(contentView) switch style { case .alert: contentView.backgroundColor = .white contentView.layer.cornerRadius = 2 contentView.snp.makeConstraints { make in make.center.equalToSuperview() make.width.equalTo(PopupViewConfig.width) } layoutAlertStyle() case .sheet: contentView.snp.makeConstraints { make in make.left.right.bottom.equalToSuperview() } layoutSheetStyle() } } func layoutAlertStyle() { var top = PopupViewConfig.vMargin var actionOnlyInTopRight = true if actions.contains(where: { $0.position == .topRight }) { top = PopupViewConfig.closeButtonVMargin } if actions.contains(where: { $0.position == .bottom }) { actionOnlyInTopRight = false } if let headlineLabel = headlineLabel { contentView.addSubview(headlineLabel) headlineLabel.snp.makeConstraints { make in make.top.equalToSuperview().offset(top) make.left.equalToSuperview().offset(PopupViewConfig.hMargin) } } let hLine = UIView() if !actionOnlyInTopRight { hLine.backgroundColor = PopupViewConfig.buttonBorderColor contentView.addSubview(hLine) hLine.snp.makeConstraints { make in make.height.equalTo(0.5) make.right.left.equalToSuperview() } } if let messageLabel = messageLabel { contentView.addSubview(messageLabel) messageLabel.snp.makeConstraints { make in if let headlineLabel = headlineLabel { make.top.equalTo(headlineLabel.snp.bottom).offset(PopupViewConfig.vSpace) } else { make.top.equalToSuperview().offset(top) } make.left.equalToSuperview().offset(PopupViewConfig.hMargin) make.right.equalToSuperview().offset(-PopupViewConfig.hMargin) if !actionOnlyInTopRight { make.bottom.equalTo(hLine.snp.top).offset(-PopupViewConfig.vMargin) } else { make.bottom.equalToSuperview().offset(-PopupViewConfig.vSpace) } } } if let textField = inTextField { contentView.addSubview(textField) textField.snp.makeConstraints { make in if let headlineLabel = headlineLabel { make.top.equalTo(headlineLabel.snp.bottom).offset(PopupViewConfig.vSpace) } else { make.top.equalToSuperview().offset(PopupViewConfig.vMargin) } make.left.equalToSuperview().offset(PopupViewConfig.hMargin) make.right.equalToSuperview().offset(-PopupViewConfig.hMargin) make.height.equalTo(PopupViewConfig.textFieldHeight) var tipsHeight: CGFloat = 0 if tipsLabel != nil { tipsHeight = 15.0 } if !actionOnlyInTopRight { var bottomOffset = PopupViewConfig.vMargin + tipsHeight make.bottom.equalTo(hLine.snp.top).offset(-bottomOffset) } else { var bottomOffset = PopupViewConfig.vSpace + tipsHeight make.bottom.equalToSuperview().offset(-bottomOffset) } } if let tipsLabel = tipsLabel { contentView.addSubview(tipsLabel) tipsLabel.snp.makeConstraints { make in make.top.equalTo(textField.snp.bottom).offset(PopupViewConfig.textFieldTipsVMargin) make.left.equalToSuperview().offset(PopupViewConfig.textFieldTipsHMargin) make.right.equalToSuperview().offset(-PopupViewConfig.textFieldTipsHMargin) } } } if let customView = inCustomView { contentView.addSubview(customView) customView.snp.makeConstraints { make in make.top.left.right.equalToSuperview() if !actionOnlyInTopRight { make.bottom.equalTo(hLine.snp.top) } else { make.bottom.equalToSuperview() } } } var actionsButtonInBottom: [UIButton] = [] for action in actions { let button = UIButton() button.addTarget(self, action: #selector(onTap), for: .touchUpInside) contentView.addSubview(button) actionButtons.append(button) if action.position == .topRight { button.setImage(action.image, for: .normal) button.contentEdgeInsets = PopupViewConfig.closeButtonInset button.snp.makeConstraints { make in make.top.equalToSuperview() make.right.equalToSuperview() } } else { button.setAttributedTitle(action.attributedTitle, for: .normal) actionsButtonInBottom.append(button) } } var prevButton: UIButton? for (index, button) in actionsButtonInBottom.enumerated() { if let prevButton = prevButton { let vLine = UIView() vLine.backgroundColor = PopupViewConfig.buttonBorderColor contentView.addSubview(vLine) vLine.snp.makeConstraints { make in make.width.equalTo(0.5) make.top.equalTo(prevButton) make.left.equalTo(prevButton.snp.right) make.bottom.equalToSuperview() } button.snp.makeConstraints { make in make.height.equalTo(PopupViewConfig.buttonHeight) make.top.equalTo(hLine.snp.bottom) make.left.equalTo(vLine.snp.right) make.bottom.equalToSuperview() make.width.equalTo(prevButton) if index == actionsButtonInBottom.count - 1 { make.right.equalToSuperview() } } } else { button.snp.makeConstraints { make in make.height.equalTo(PopupViewConfig.buttonHeight) make.top.equalTo(hLine.snp.bottom) make.left.equalToSuperview() make.bottom.equalToSuperview() if index == actionsButtonInBottom.count - 1 { make.right.equalToSuperview() } } } prevButton = button } } func layoutSheetStyle() { if let customView = inCustomView { contentView.addSubview(customView) customView.snp.makeConstraints { make in make.edges.equalToSuperview() } } } @objc func onDismiss() { hide() } @objc func onTap(button: UIButton) { if let index = actionButtons.firstIndex(where: { $0 == button }) { let action = actions[index] if action.autoDismiss { hide { if let handler = action.handler { handler(action) } } } else { if let handler = action.handler { handler(action) } } } } } extension PopupViewController: UIViewControllerTransitioningDelegate { public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { if let presentAnimator = presentAnimator { return presentAnimator } else if style == .alert { return PopupAlertAnimator() } else if style == .sheet { return PopupSheetAnimator() } return nil } public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { if let dimsissAnimator = dimsissAnimator { return dimsissAnimator } else if style == .alert { let animator = PopupAlertAnimator() animator.isPresented = false return animator } else if style == .sheet { let animator = PopupSheetAnimator() animator.isPresented = false return animator } return nil } } extension PopupViewController: PopupAnimatable { public var animateContentView: UIView { return contentView } public var animateOverlayView: UIView { return overlayView } }