Swift - 顶部弹出框封装

抽空把项目里的顶部弹出框封装了一下。

效果大概是这个样子:

代码

自定义弹出框:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
//
// KKHeadMessageView.swift
// VoiceAssistant
//
// Created by Kai Lv on 2020/7/29.
// Copyright © 2020 Kaaaaai. All rights reserved.
//

import UIKit

@objc public enum MessageStyle: Int{
case none
case success
case error
case warning
}


@objc extension KKHeadMessageView{
static var mv: KKHeadMessageView?

@objc public class func showMessageView(_ message: String) -> (){
if mv != nil {
mv?.removeFromSuperview()
mv = nil
}

mv = KKHeadMessageView.init(message: message, style: .warning)
mv!.show()
}

@objc public class func showMessageView(_ message: String, style:MessageStyle) -> (){
if mv != nil {
mv?.removeFromSuperview()
mv = nil
}

mv = KKHeadMessageView.init(message: message, style: style)
mv!.show()
}
}

@objc public class KKHeadMessageView: UIView {

private let message: String
private var label_mes = UILabel.init()

private var style: MessageStyle = .none

private static let successBackgroundColor: UIColor = UIColor(red: 86.0/255, green: 188/255, blue: 138.0/255, alpha: 1)
private static let warningBackgroundColor: UIColor = UIColor(red: 242.0/255, green: 153.0/255, blue: 46.0/255, alpha: 1)
private static let errorBackgroundColor: UIColor = UIColor(red: 192.0/255, green: 36.0/255, blue: 37.0/255, alpha: 1)
private static let noneBackgroundColor: UIColor = UIColor(red: 44.0/255, green: 187.0/255, blue: 255.0/255, alpha: 1)

private init(message: String, style: MessageStyle){
self.message = message
self.style = style

super.init(frame: CGRect(x: 0, y: -49, width: UIScreen.main.bounds.size.width, height: 48))

switch style {
case .success:
self.backgroundColor = KKHeadMessageView.successBackgroundColor
break
case .warning:
self.backgroundColor = KKHeadMessageView.warningBackgroundColor
break
case .error:
self.backgroundColor = KKHeadMessageView.errorBackgroundColor
break
case .none:
self.backgroundColor = KKHeadMessageView.noneBackgroundColor
break
}

let attributes = [NSAttributedString.Key.font : UIFont.init(name: "PingFangSC-Medium", size: 14)]

let textSize = self.message.boundingRect(with: CGSize(width: UIScreen.main.bounds.size.width, height: CGFloat.greatestFiniteMagnitude), options: NSStringDrawingOptions.usesLineFragmentOrigin, attributes: attributes as [NSAttributedString.Key : Any], context: nil).size

let label_mes = UILabel.init(frame: CGRect(x: 10, y: self.frame.height - textSize.height - 5, width: textSize.width, height: textSize.height))
label_mes.font = UIFont.init(name: "PingFangSC-Medium", size: 14)
label_mes.textColor = .white
label_mes.text = message
self.label_mes = label_mes
self.addSubview(self.label_mes)
}

func show(){
UIApplication.shared.keyWindow?.addSubview(self)

let animations : () -> () = {
self.frame.origin.y = -4
}

let completionAnimations : (Bool) -> () = { finished in
if finished {
UIView.animate(withDuration: 5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 5, options: [.curveEaseInOut], animations: {
self.frame.origin.y = -49
}){finished in
self.removeFromSuperview()
}
}
}

UIView.animate(withDuration: 1, delay: 0, usingSpringWithDamping: 10, initialSpringVelocity: 10, options: [.curveEaseInOut], animations: animations, completion: completionAnimations)

}

/**
NSCoding not supported. Use init(text, preferences, delegate) instead!
*/
required public init?(coder aDecoder: NSCoder) {
fatalError("NSCoding not supported. Use init(text, preferences, delegate) instead!")
}
}

调用方式:

1
2
3
4
5
//Swift 调用
KKHeadMessageView.showMessageView("😊这是一个测试的弹窗。", style: .success)

//OC 调用
[KKHeadMessageView showMessageView:@"😊这是一个测试的弹窗" style:MessageStyleNone];

一些思考

如上文所见,基本逻辑相对简单,首先编写一个继承自 UIView 的类,重写 init 方法用来自定义这个 view,为了方便调用,用拓展写了两个类方法 showMessageView(),让后通过定义的枚举来展示不同风格的提示框。虽然作为一个自定义 View,上面的代码可能不是最优写法,但勉强算最适合我当前项目的写法。

也考虑到这个自定义 View,并不对多数人适用。所以想分享一下我在封装这个 View 时所做的一些思考,主要有两点:

  • 1.如何写好一个自定义 View?
  • 2.我们应该如何设定自定义 View 的调用方式?

那么关于这两个问题的答案从哪里可以找到呢?其实总的来说也就是老生常谈的三个方向:

  1. 官方文档。
  2. 阅读源码。
  3. 技术博客。

展开来说,自定义 View 的官方文档,其实就是去看看 UIButtonUIView 官方是怎么写的。但因为苹果是不开源的,看不到——虽然网上也有反推的实现,但毕竟有作者个人风格,而且有些还是用 C++ 实现的,要模仿着写比较费劲。所以这条先搁置。

然后退而求其次,阅读 GitHub 上 Star 数量较多的自定义视图开源库,学学他们的写法。这里推荐两个库:

在封装消息提示框的时候我也考虑过对外开放的接口做成 MBProgressHUD 中调用方法返回一个单例,或者是和 SwiftMessages 将视图定义和使用分开。但最终因为图省事就选了一个简单的方式。另外还想到一个把 MessageView 用枚举来定义,然后通过实现关联值方法来直接显示,如果换这种实现的话,上面的调用方式可能就会变成这样:

1
KKHeadMessageView.success.showMessageView("😊这是一个测试的弹窗。")

看起来有点像工厂模式的产物😊。

最后是技术博客。在 Google 上搜,一大推的教你怎么自定义 View,这里推荐两篇我觉得比较好的,有概念讲解,有源码示范,值得参考:

最后还有几个小疑惑备注在这里:

  1. 在自定义 View 中,我们通常在 setSubUI 方法里面设置子视图,在里面写一大堆类似于这种代码:
    1
    2
    3
    4
    5
    6
    let label_mes = UILabel.init(frame: CGRect(x: 10, y: self.frame.height - textSize.height - 5, width: textSize.width, height: textSize.height))
    label_mes.font = UIFont.init(name: "PingFangSC-Medium", size: 14)
    label_mes.textColor = .white
    label_mes.text = message
    self.label_mes = label_mes
    self.addSubview(self.label_mes)
    iOS应用架构谈 view层的组织和调用方案 中看到说建议把这种初始化都丢进 getter 里面,而且 getter 和 setter 全部都放在最后。另外配合 Swift 中的 extension 可以让代码布局更好看,但也看到唐巧的iOS 开发中的争议(一)中说:

    在类中完全使用 _property 的方式来访问私有成员变量,是不会有内存管理上的问题的。但是使用 self.property 的方式来访问私有变量是不是也是一样不会有内存管理上的问题呢?确实也是,但是有一点需要注意:我们最好不要在 init 和 dealloc 中使用 self.property 的方式来访问成员变量。

那么 setSubUI 的写法最优解是什么呢?

  1. 一个自定义 View 的展现形式通常有:
    1)外部初始化后,外部手动添加到视图,比如一些嵌入视图
    2)外部初始化后,自动添加到 UIWindow 或者当前显示 Controller,比如一些弹出框视图。
    3)也看到一自定义 View 会定义这种初始化方法initWith:(NSString*)Message ToRootView:(UIView*)View。外部传入父视图,然后自己在内部添加自己。
    以上三种方式,除了根据实际需求,就把自己添加到父视图上,还有什么更好的方式吗?

以上。