如何设计一个优雅的弹框,Swift版
2020-05-06 本文已影响0人
何以消摇
理念:爽到使用者就行了
最爽的 调用弹框,那肯定是:Alert().show()
什么,你要手动隐藏?那我再提供一个 dismiss()
函数吧。
什么,你还要改背景颜色,弹框动画的方向,还要能自定义……
思路:通过协议的方式,提供默认实现
行行行,都满足你。我提供一个协议给你,然后帮你实现默认的动画,剩下的你自己发挥想象力就好了。
协议(AlertProtocol.swift):
public enum AppearFrom {
case top, bottom, left, right
}
// Protocol for showing and dissmising alert view
public protocol AlertProtocol {
func show(animated:Bool) -> Self
func dismiss(animated:Bool) -> Self
var backgroundView: UIView {get}
var dialogView: UIView {get set}
var appearFrom: AppearFrom {get}
var clearBackground: Bool {get}
}
extension AlertProtocol {
@discardableResult
public func show() -> Self {
return show(animated: true)
}
@discardableResult
public func dismiss() -> Self {
return dismiss(animated: true)
}
}
默认实现 show
和 dismiss
extension AlertProtocol where Self: UIView{
@discardableResult
public func show(animated: Bool) -> Self {
return show(animated: animated, superview: nil)
}
@discardableResult
public func show(animated: Bool, superview: UIView?) -> Self {
// Set origin before Animation
if appearFrom == .top {
self.dialogView.center = CGPoint(x:self.center.x, y:-self.frame.height+self.dialogView.frame.size.height/2)
}else if appearFrom == .bottom {
self.dialogView.center = CGPoint(x:self.center.x, y:self.frame.height+self.dialogView.frame.size.height/2)
}else if appearFrom == .left {
self.dialogView.center = CGPoint(x:-self.frame.size.width, y:self.frame.height/2)
}else {
self.dialogView.center = CGPoint(x:self.frame.size.width, y:self.frame.height/2)
}
self.backgroundView.alpha = 0
self.backgroundView.isHidden = false
if superview != nil {
superview?.addSubview(self)
}else {
let cv = UIViewController.currentViewController()
if let nav = cv?.navigationController {
nav.view.addSubview(self)
}else {
cv?.view.addSubview(self)
}
}
if animated {
UIView.animate(withDuration: 0.33, animations: {
if self.clearBackground == true{
self.backgroundView.alpha = 0
}else{
self.backgroundView.alpha = 0.6
}
})
// Set origin during Animation
UIView.animate(withDuration: 0.33, delay:0, usingSpringWithDamping:0.7, initialSpringVelocity:10, options:UIView.AnimationOptions(rawValue:0), animations: {
self.dialogView.center = self.center
})
}else{
if self.clearBackground == true{
self.backgroundView.alpha = 0
}else{
self.backgroundView.alpha = 0.6
}
self.dialogView.center = self.center
}
return self
}
@discardableResult
public func dismiss() -> Self {
return dismiss(animated: true)
}
@discardableResult
public func dismiss(animated: Bool) -> Self {
self.backgroundView.isHidden = true
if animated {
UIView.animate(withDuration: 0.33, animations: {
self.backgroundView.alpha = 0
})
UIView.animate(withDuration: 0.33, delay:0, usingSpringWithDamping: 1, initialSpringVelocity:10, options:UIView.AnimationOptions(rawValue:0), animations: {
if self.appearFrom == .top {
self.dialogView.center = CGPoint(x:self.center.x, y:-self.frame.height+self.backgroundView.frame.size.height/2)
}else if self.appearFrom == .bottom {
self.dialogView.center = CGPoint(x:self.center.x, y:self.frame.height+self.dialogView.frame.size.height/2)
}else if self.appearFrom == .left {
self.dialogView.center = CGPoint(x:-self.frame.size.width, y:self.frame.height/2)
}else {
self.dialogView.center = CGPoint(x:self.frame.size.width+self.dialogView.frame.size.width, y:self.frame.height/2)
}
}) { (completed) in
self.removeFromSuperview()
}
}else{
self.removeFromSuperview()
}
return self
}
}
extension UIViewController {
/// 获取当前最顶层的vc
internal class func currentViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return currentViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController {
return currentViewController(base: tab.selectedViewController)
}
if let presented = base?.presentedViewController {
return currentViewController(base: presented)
}
return base
}
}
进化:提供一个容器
使用者吐槽,你在逗我吗?这玩意怎么用啊,你这也太敷衍了吧,我还要写一大堆代码,才能用。
好吧,那我给你一个默认的容器吧,你只要告诉我中间显示啥就可以了。
调用1:
let base = OrderPayView.nibView().setTotalL("1000")
let al = AlertCustomView<OrderPayView>(base)
.show(animated: true)
base.payBtn.rx.tap.bind { [unowned al] in
al.dismiss()
print("pay click")
}.disposed(by: bag)
调用2:
let base = UILabel()
base.text = "1000"
AlertCustomView(base, false).show(animated: true)
效果:
效果1
效果2
容器代码(AlertCustomView.swift):
/// 自定义弹框
open class AlertCustomView<Base: UIView>: UIView, AlertProtocol {
/// 自定义的组件
open var base: Base {
get { return customView }
set { customView = newValue }
}
// MARK: - datas
public var appearFrom: AppearFrom = .right
public var clearBackground = Bool()
public var closeBtnClick: (() -> Void)?
// MARK: - views
public var backgroundView = UIView()
public var dialogView = UIView()
// 自定义的View
fileprivate var customView: Base = Base()
/// 是否隐藏close
private var isHiddenClose: Bool = true
private var closeBtn: UIButton! // 关闭
// MARK: - leftStyle
deinit {
print("alertType deinit")
}
fileprivate override init(frame: CGRect) {
super.init(frame: frame)
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initViews()
latoutUI()
}
public convenience init(_ customView: Base, _ isHiddenClose: Bool = true) {
self.init(frame: UIScreen.main.bounds)
self.customView = customView
self.isHiddenClose = isHiddenClose
initViews()
latoutUI()
}
// MARK: - events
// dismiss the alert view
@objc public func didcloseBtnTapped() {
dismiss(animated: true)
closeBtnClick?()
}
@objc func backgroundViewTap() {
print("backgroundViewTap")
}
fileprivate func latoutUI() {
let cf = base.frame
backgroundView.frame = frame
addSubview(backgroundView)
addSubview(dialogView)
dialogView.addSubview(customView)
dialogView.addSubview(closeBtn)
dialogView.snp.makeConstraints { (make) in
make.center.equalToSuperview()
}
customView.snp.makeConstraints { (make) in
make.size.equalTo(cf.size).priority(.low)
let bottom: CGFloat = isHiddenClose ? 0 : 54.5
make.edges.equalTo(UIEdgeInsets(top: cf.origin.y, left: cf.origin.x, bottom: bottom, right: 0 ))
}
closeBtn.snp.makeConstraints { (make) in
make.bottom.centerX.equalToSuperview()
make.width.height.equalTo(44)
}
}
// MARK: - initViews
public func initViews() {
backgroundView = {
let backgroundView = UIView()
backgroundView.backgroundColor = UIColor.black
return backgroundView
}()
let tap = UITapGestureRecognizer(target: self,
action: #selector(backgroundViewTap))
backgroundView.addGestureRecognizer(tap)
closeBtn = {
let closeBtn = UIButton()
closeBtn.setImage(UIImage(named: "close"), for: .normal)
return closeBtn
}()
closeBtn.isHidden = isHiddenClose
closeBtn.addTarget(self, action: #selector(didcloseBtnTapped), for: UIControl.Event.touchUpInside)
}
}
什么,你还是觉得丑,想要一个能改的默认模版?喂喂喂,过分了,兄弟,是你要求要自定义的。
既然写到了这里,我就勉为骑男的再帮你写个默认实现吧。
超进化:提供默认弹框
事先说好,我用到了snpkit,你要是没有导入,那自己计算frame吧
假装是代码
这个类有点长,放最后面吧。
这里要说的一点是提供了一个样式类,都有默认值,需要修改的,设置一下内容的style就好了。
AlertViewStyle.swift
public struct AlertViewStyle {
public var backgroundViewAlpha: CGFloat = 0.6
public var dialogViewCornerRadius: CGFloat = 5
public var titleLFont: CGFloat = 18
public var titleLColor = UIColor.hex("#333333")
public var messageLFont: CGFloat = 15
public var messageLColor = UIColor.hex("#333333")
public var textViewFont: CGFloat = 15
public var textViewBorderColor = UIColor.hex("#F07D8A")
public var textViewTextCorlr = UIColor.hex("#333333")
public var cancelBtnTextColor = UIColor.hex("#999999")
public var cancelBtnTextFont: CGFloat = 16
public var cancelBtnNormalBgColor = UIColor.hex("#EEEFF1")
// public var cancelBtnHighlightedBgColor = UIColor.c192451
public var doneBtnTextColor = UIColor.white
public var doneBtnTextFont: CGFloat = 16
public var doneBtnNormalBgColor = UIColor.hex("#F07D8A")
public var doneBtnHighlightedBgColor = UIColor.hex("#F07D8A", alpha: 0.8)
public var cornerRadius: CGFloat = 3
public init() {}
}
效果图:
image.png
image.png
什么,你还要加入业务?!
究极进化:整合业务
调用
let al = AlertTipBusinessView.init(.giveupPay).show()
al.base.doneBtnClick = { [unowned al] in
al.dismiss(animated: true)
}
详细代码(AlertTipBusinessView.swift):
/// 业务类型
enum TipBusinessType {
case giveupPay
case addAddress
case deleteAddress
case cancelOrder
case remindDeliverGoods
case secendRemind
struct Data {
var title = ""
var subTitle = ""
var certain = ""
var other = ""
init(subTitle: String, centain: String = "确认", title: String = "", other: String = "") {
self.title = title
self.subTitle = subTitle
self.certain = centain
self.other = other
}
init() {
self.init(subTitle: "")
}
}
var data: Data {
var data = Data(subTitle: "")
switch self {
case .giveupPay:
data.title = "确认要离开吗?"
data.subTitle = "心动美物,看中了就赶紧下手哦!"
data.certain = "继续购物"
data.other = "残忍离开"
case .addAddress:
data.title = "添加地址"
data.subTitle = "您尚未添加收件地址"
data.certain = "添加"
data.other = "稍后"
case .deleteAddress:
data.title = "删除地址"
data.subTitle = "删除地址后不能恢复,请谨慎操作"
data.certain = "返回"
data.other = "删除"
case .cancelOrder:
data.title = "确认收货"
data.subTitle = "请在收到货之后再确认收货"
data.certain = "确认收货"
data.other = "取消"
case .remindDeliverGoods:
data.title = "提醒发货"
data.subTitle = "平台已经收到您的提醒,会尽快发货,请注意查收系统消息和短信"
data.certain = "我知道了"
case .secendRemind:
data.subTitle = "平台已经收到您的提醒\n请勿频繁提交发货提醒"
data.certain = "我知道了"
}
return data
}
}
/// 提示业务逻辑
class AlertTipBusinessView: AlertCustomView<AlertTipView> {
/// 普通用法
/// AlertTipBusinessView.init(.secendRemind).show()
///
/// 自定义按钮事件
/// let al = AlertTipBusinessView.init(.secendRemind).show()
/// al.base.doneBtnClick = { [unowned al] in
/// al.dismiss(animated: true)
/// }
convenience init(_ businessType: TipBusinessType) {
let data = businessType.data
let tipV = AlertTipView(title: data.title, message: data.subTitle, doneBtnTitle: data.certain, cancelBtnTitle: data.other)
self.init(tipV)
if data.other.isEmpty {
tipV.doneBtnClick = { [unowned self] in
self.dismiss()
}
}else {
tipV.cancelBtnClick = { [unowned self] in
self.dismiss()
}
}
}
}
合体:其他代码
自定义UI代码(AlertTipView.swift):
import Foundation
import UIKit
public class AlertTipView: UIView {
// MARK: - datas
public var style = AlertViewStyle() {
didSet {
titleL.font = UIFont.boldSystemFont(ofSize: style.titleLFont)
titleL.textColor = style.titleLColor
if titleL.text?.isEmpty ?? true {
messageL.font = UIFont.systemFont(ofSize: style.titleLFont)
}else {
messageL.font = UIFont.systemFont(ofSize: style.messageLFont)
}
messageL.textColor = style.messageLColor
doneBtn.setTitleColor(style.doneBtnTextColor, for: .normal)
doneBtn.titleLabel?.font = UIFont.boldSystemFont(ofSize: style.doneBtnTextFont)
doneBtn.setBackgroundImage(UIImage(color: style.doneBtnNormalBgColor),
for: .normal)
doneBtn.setBackgroundImage(UIImage(color: style.doneBtnHighlightedBgColor),
for: .highlighted)
cancelBtn.titleLabel?.font = UIFont.boldSystemFont(ofSize: style.cancelBtnTextFont)
cancelBtn.setTitleColor(style.cancelBtnTextColor, for: .normal)
cancelBtn.setBackgroundImage(UIImage(color: style.cancelBtnNormalBgColor),
for: .normal)
}
}
public var doneBtnClick: (() -> Void)?
public var cancelBtnClick: (() -> Void)?
// MARK: - views
public var whiteBgV: UIView!
private var titleL: UILabel!
private var messageL: UILabel!
public var cancelBtn: UIButton! // 取消
public var doneBtn: UIButton!
// MARK: - leftStyle
deinit {
print("alertType deinit")
}
public convenience init(title: String, message: String, doneBtnTitle: String, cancelBtnTitle: String = "取消") {
self.init(frame: CGRect(x: 0, y: 0, width: 265, height: 0))
self.initialise(title: title, message: message, doneBtnTitle: doneBtnTitle, cancelBtnTitle: cancelBtnTitle)
}
public override init(frame: CGRect) {
style = AlertViewStyle()
super.init(frame: frame)
initViews()
}
public required init?(coder aDecoder: NSCoder) {
style = AlertViewStyle()
super.init(coder: aDecoder)
initViews()
}
private func initialise(title: String, message: String, doneBtnTitle: String, cancelBtnTitle: String = "取消") {
titleL.text = title
doneBtn.setTitle(doneBtnTitle, for: UIControl.State.normal)
cancelBtn.setTitle(cancelBtnTitle, for: UIControl.State.normal)
messageL.text = message
cancelBtn.addTarget(self, action: #selector(didCancelBtnTapped), for: UIControl.Event.touchUpInside)
doneBtn.addTarget(self, action: #selector(didDoneBtnTappad), for: UIControl.Event.touchUpInside)
let hasCancel = !cancelBtnTitle.isEmpty
hasCancel ? setupTextAlert(!title.isEmpty) : setupNoCancelTextAlert(!title.isEmpty)
}
// MARK: - events
@objc public func didDoneBtnTappad() {
print("doneBtn isTappad,get btn use getdoneBtn()")
doneBtnClick?()
}
// dismiss the alert view
@objc public func didCancelBtnTapped() {
cancelBtnClick?()
}
// MARK: - initViews
public func initViews() {
whiteBgV = {
let view = UIView()
view.backgroundColor = UIColor.white
view.layer.cornerRadius = style.cornerRadius
view.clipsToBounds = true
return view
}()
titleL = {
let titleL = UILabel()
titleL.textAlignment = .center
titleL.lineBreakMode = NSLineBreakMode.byWordWrapping
titleL.numberOfLines = 0
titleL.sizeToFit()
return titleL
}()
messageL = {
let messageL = UILabel()
messageL.numberOfLines = 0
messageL.lineBreakMode = NSLineBreakMode.byWordWrapping
messageL.textAlignment = .center
messageL.sizeToFit()
return messageL
}()
cancelBtn = {
let cancelBtn = UIButton()
cancelBtn.layer.cornerRadius = style.cornerRadius
cancelBtn.clipsToBounds = true
return cancelBtn
}()
doneBtn = {
let doneBtn = UIButton()
// doneBtn.backgroundColor = UIColor.darkText
doneBtn.layer.cornerRadius = style.cornerRadius
doneBtn.clipsToBounds = true
return doneBtn
}()
}
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("al touchesBegan")
}
}
// MARK: - textAlert
extension AlertTipView {
fileprivate func setupTextAlert(_ hasTitle: Bool = true) {
self.addSubview(whiteBgV)
whiteBgV.addSubview(titleL)
whiteBgV.addSubview(messageL)
whiteBgV.addSubview(doneBtn)
whiteBgV.addSubview(cancelBtn)
whiteBgV.snp.makeConstraints { (make) in
make.left.top.right.equalToSuperview()
make.bottom.equalTo(0)
make.width.equalTo(265)
}
titleL.snp.makeConstraints { (make) in
make.top.equalTo(20)
make.left.equalTo(15)
make.right.equalTo(-15)
}
messageL.snp.makeConstraints { (make) in
let top: CGFloat = hasTitle ? 60 : 40
let bottom: CGFloat = hasTitle ? 70 : 84
make.edges.equalTo(UIEdgeInsets(top: top, left: 20, bottom: bottom, right: 20))
}
doneBtn.snp.makeConstraints { (make) in
make.height.equalTo(44)
make.right.bottom.equalTo(0)
make.width.equalToSuperview().multipliedBy(0.5)
}
cancelBtn.snp.makeConstraints { (make) in
make.left.equalTo(0)
make.bottom.width.height.equalTo(doneBtn)
}
doneBtn.clipsToBounds = false
cancelBtn.clipsToBounds = false
}
fileprivate func setupNoCancelTextAlert(_ hasTitle: Bool = true) {
self.addSubview(whiteBgV)
whiteBgV.addSubview(titleL)
whiteBgV.addSubview(messageL)
whiteBgV.addSubview(doneBtn)
whiteBgV.addSubview(cancelBtn)
whiteBgV.snp.makeConstraints { (make) in
make.left.top.right.equalToSuperview()
make.bottom.equalTo(0)
make.width.equalTo(265)
}
titleL.snp.makeConstraints { (make) in
make.top.equalTo(20)
make.left.equalTo(15)
make.right.equalTo(-15)
}
messageL.snp.makeConstraints { (make) in
let top: CGFloat = hasTitle ? 60 : 40
let bottom: CGFloat = hasTitle ? 80 : 96
make.edges.equalTo(UIEdgeInsets(top: top, left: 20, bottom: bottom, right: 20))
}
doneBtn.snp.makeConstraints { (make) in
make.height.equalTo(36)
make.bottom.equalTo(-20)
make.left.equalTo(22.5)
make.right.equalTo(-22.5)
}
}
}
项目中用到的小工具(AlertTools.swift)
import Foundation
//mark UIImage with downloadable content
extension UIImage {
//根据颜色创建图片
public convenience init?(color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) {
let rect = CGRect(origin: .zero, size: size)
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
color.setFill()
UIRectFill(rect)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
guard let cgImage = image?.cgImage else { return nil }
self.init(cgImage: cgImage)
}
}
extension UIColor {
public convenience init(hex: String) {
self.init(hex: hex, alpha:1)
}
public convenience init(hex: String, alpha: CGFloat) {
var hexWithoutSymbol = hex
if hexWithoutSymbol.hasPrefix("#") {
hexWithoutSymbol = hex.substring(from: 1)
}
let scanner = Scanner(string: hexWithoutSymbol)
var hexInt:UInt32 = 0x0
scanner.scanHexInt32(&hexInt)
var r:UInt32!, g:UInt32!, b:UInt32!
switch (hexWithoutSymbol.count) {
case 3: // #RGB
r = ((hexInt >> 4) & 0xf0 | (hexInt >> 8) & 0x0f)
g = ((hexInt >> 0) & 0xf0 | (hexInt >> 4) & 0x0f)
b = ((hexInt << 4) & 0xf0 | hexInt & 0x0f)
break;
case 6: // #RRGGBB
r = (hexInt >> 16) & 0xff
g = (hexInt >> 8) & 0xff
b = hexInt & 0xff
break;
default:
print("UIColor init error: hex == \(hex), alpha == \(alpha)")
break;
}
self.init(
red: (CGFloat(r)/255),
green: (CGFloat(g)/255),
blue: (CGFloat(b)/255),
alpha:alpha)
}
public static func hex(_ hex: String) -> UIColor{
return UIColor(hex: hex)
}
public static func hex(_ hex: String, alpha: CGFloat) -> UIColor{
return UIColor(hex: hex, alpha: alpha)
}
}
// MARK: - substring
extension String {
/// 返回Index类型
///
public func index(from: Int) -> Index {
return self.index(startIndex, offsetBy: from)
}
/// 裁剪字符串from
///
/// - Parameter from: 从哪里开始
public func substring(from: Int) -> String {
let fromIndex = index(from: from)
return String(suffix(from: fromIndex))
}
/// 裁剪字符串to
///
/// - Parameter to: 到哪里结束
public func substring(to: Int) -> String {
let toIndex = index(from: to)
return String(prefix(upTo: toIndex))
}
}